diff --git a/.bazelignore b/.bazelignore deleted file mode 100644 index 284b0692ec13..000000000000 --- a/.bazelignore +++ /dev/null @@ -1,3 +0,0 @@ -.git -dist -node_modules diff --git a/.bazelrc b/.bazelrc index 12d0607308da..92e60820829a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,5 +1,9 @@ # Disable NG CLI TTY mode -test --action_env=NG_FORCE_TTY=false +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. @@ -8,6 +12,13 @@ build --strategy=TypeScriptCompile=worker # Enable debugging tests with --config=debug test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results +# Enable debugging tests with --config=no-sharding +# The below is useful to while using `fit` and `fdescribe` to avoid sharing and re-runs of failed flaky tests. +test:no-sharding --flaky_test_attempts=1 --test_sharding_strategy=disabled + +# Frozen lockfile +common --lockfile_mode=error + ############################### # Filesystem interactions # ############################### @@ -25,24 +36,28 @@ test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test # See https://github.com/bazelbuild/bazel/issues/4603 build --symlink_prefix=dist/ -# Disable watchfs as it causes tests to be flaky on Windows -# https://github.com/angular/angular/issues/29541 -build --nowatchfs - # Turn off legacy external runfiles -run --nolegacy_external_runfiles -test --nolegacy_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 `yarn bazel`. +# 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 --incompatible_allow_tags_propagation + +# Ensure sandboxing is enabled even for exclusive tests +test --incompatible_exclusive_test_sandboxed + ############################### # Saucelabs support # # Turn on these settings with # @@ -68,7 +83,21 @@ test:saucelabs --define=KARMA_WEB_TEST_MODE=SL_REQUIRED # 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="yarn -s ng-dev release build-env-stamp" +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 # @@ -80,20 +109,13 @@ query --output=label_kind # By default, failing tests don't print any output, it goes to the log file test --test_output=errors - -################################ -# Settings for CircleCI # -################################ - -# Bazel flags for CircleCI are in /.circleci/bazel.rc - ################################ # Remote Execution Setup # ################################ # Use the Angular team internal GCP instance for remote execution. -build:remote --remote_instance_name=projects/internal-200822/instances/default_instance -build:remote --project_id=internal-200822 +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 @@ -107,14 +129,13 @@ 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 --crosstool_top=@npm//@angular/dev-infra-private/bazel/remote-execution/cpp:cc_toolchain_suite -build:remote --extra_toolchains=@npm//@angular/dev-infra-private/bazel/remote-execution/cpp:cc_toolchain -build:remote --extra_execution_platforms=//tools:rbe_platform_with_network_access -build:remote --host_platform=//tools:rbe_platform_with_network_access -build:remote --platforms=//tools:rbe_platform_with_network_access +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. @@ -124,10 +145,31 @@ build:remote --host_cpu=k8 # Set up authentication mechanism for RBE build:remote --google_default_credentials -############################### -# NodeJS rules settings -# These settings are required for rules_nodejs -############################### +# 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 @@ -137,7 +179,3 @@ build:remote --google_default_credentials # 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 - -# Enable runfiles even on Windows. -# Architect resolves output files from data files, and this isn't possible without runfile support. -test --enable_runfiles diff --git a/.bazelversion b/.bazelversion index fcdb2e109f68..6d2890793d47 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -4.0.0 +8.5.0 diff --git a/.circleci/bazel.rc b/.circleci/bazel.rc deleted file mode 100644 index 1b89d0bd6424..000000000000 --- a/.circleci/bazel.rc +++ /dev/null @@ -1,24 +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 "xlarge" class -# https://circleci.com/docs/2.0/configuration-reference/#resource_class -build --local_cpu_resources=8 -build --local_ram_resources=14336 - -# More details on failures -build --verbose_failures=true - -# 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 400fcc82e988..000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,358 +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/ - -version: 2.1 - -orbs: - browser-tools: circleci/browser-tools@1.0.1 - -# Variables - -## IMPORTANT -# Windows needs its own cache key because binaries in node_modules are different. -# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI. -var_1: &cache_key v1-angular_devkit-14.15-{{ checksum "yarn.lock" }} -var_1_win: &cache_key_win v1-angular_devkit-win-12.22-{{ checksum "yarn.lock" }} -var_3: &default_nodeversion '14.15' -# Workspace initially persisted by the `setup` job, and then enhanced by `setup-and-build-win`. -# https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs -# https://circleci.com/blog/deep-diving-into-circleci-workspaces/ -var_4: &workspace_location . -# Filter to only release branches on a given job. -var_5: &only_release_branches - filters: - branches: - only: - - master - - /\d+\.\d+\.x/ - -# Executor Definitions -# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors -executors: - action-executor: - parameters: - nodeversion: - type: string - default: *default_nodeversion - docker: - - image: cimg/node:<< parameters.nodeversion >> - working_directory: ~/ng - resource_class: small - - test-executor: - parameters: - nodeversion: - type: string - default: *default_nodeversion - docker: - - image: cimg/node:<< parameters.nodeversion >> - working_directory: ~/ng - resource_class: large - - windows-executor: - # Same as https://circleci.com/orbs/registry/orb/circleci/windows, but named. - working_directory: ~/ng - resource_class: windows.medium - shell: powershell.exe -ExecutionPolicy Bypass - machine: - # Contents of this image: - # https://circleci.com/docs/2.0/hello-world-windows/#software-pre-installed-in-the-windows-image - image: windows-server-2019-vs2019:stable - -# Command Definitions -# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-commands -commands: - custom_attach_workspace: - description: Attach workspace at a predefined location - steps: - - attach_workspace: - at: *workspace_location - setup_windows: - steps: - - run: nvm install 12.22.1 - - run: nvm use 12.22.1 - - run: npm install -g yarn@1.22.10 - - run: node --version - - run: yarn --version - - setup_bazel_rbe: - parameters: - key: - type: env_var_name - default: CIRCLE_PROJECT_REPONAME - steps: - - run: - name: 'Setup bazel RBE remote execution' - command: | - touch .bazelrc.user; - # We need ensure that the same default digest is used for encoding and decoding - # with openssl. Openssl versions might have different default digests which can - # cause decryption failures based on the openssl version. https://stackoverflow.com/a/39641378/4317734 - openssl aes-256-cbc -d -in .circleci/gcp_token -md md5 -k "${<< parameters.key >>}" -out /home/circleci/.gcp_credentials; - sudo bash -c "echo -e 'build --google_credentials=/home/circleci/.gcp_credentials' >> .bazelrc.user"; - # Upload/don't upload local results to cache based on environment - if [[ -n "{$CIRCLE_PULL_REQUEST}" ]]; then - sudo bash -c "echo -e 'build:remote --remote_upload_local_results=false\n' >> .bazelrc.user"; - echo "Not uploading local build results to remote cache."; - else - sudo bash -c "echo -e 'build:remote --remote_upload_local_results=true\n' >> .bazelrc.user"; - echo "Uploading local build results to remote cache."; - fi - # Enable remote builds - sudo bash -c "echo -e 'build --config=remote' >> .bazelrc.user"; - echo "Reading from remote cache for bazel remote jobs."; - - install_python: - steps: - - run: - name: 'Install Python 2' - command: | - sudo apt-get update > /dev/null 2>&1 - sudo apt-get install -y python - python --version - -# Job definitions -jobs: - setup: - executor: action-executor - resource_class: medium - steps: - - 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 - - restore_cache: - keys: - - *cache_key - - run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - persist_to_workspace: - root: *workspace_location - paths: - - ./* - - save_cache: - key: *cache_key - paths: - - ~/.cache/yarn - - lint: - executor: action-executor - steps: - - custom_attach_workspace - - run: yarn lint - - validate: - executor: action-executor - steps: - - custom_attach_workspace - - run: - name: Validate Commit Messages - command: > - if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then - yarn ng-dev commit-message validate-range <> <> - else - echo "This build is not over a PR, nothing to do." - fi - - run: - name: Validate Code Formatting - command: yarn -s ng-dev format changed <> --check - - run: - name: Validate NgBot Configuration - command: yarn ng-dev ngbot verify - - run: - name: Validate Circular Dependencies - command: yarn ts-circular-deps:check - - run: - command: yarn -s admin validate - - e2e-cli: - parameters: - nodeversion: - type: string - default: *default_nodeversion - snapshots: - type: boolean - default: false - executor: - name: test-executor - nodeversion: << parameters.nodeversion >> - parallelism: 6 - steps: - - custom_attach_workspace - - browser-tools/install-chrome - - run: - name: Initialize Environment - command: ./.circleci/env.sh - - run: - name: Execute CLI E2E Tests - command: | - mkdir /mnt/ramdisk/e2e-main - node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-snapshots<> --tmpdir=/mnt/ramdisk/e2e-main - - run: - name: Execute CLI E2E Tests Subset with Yarn - command: | - mkdir /mnt/ramdisk/e2e-yarn - node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-snapshots<> --yarn --tmpdir=/mnt/ramdisk/e2e-yarn --glob="{tests/basic/**,tests/update/**,tests/commands/add/**}" - - test-browsers: - executor: - name: test-executor - environment: - E2E_BROWSERS: true - resource_class: medium - steps: - - custom_attach_workspace - - run: - name: Initialize Environment - command: ./.circleci/env.sh - - run: - name: Initialize Saucelabs - command: setSecretVar SAUCE_ACCESS_KEY $(echo $SAUCE_ACCESS_KEY | rev) - - run: - name: Start Saucelabs Tunnel - command: ./scripts/saucelabs/start-tunnel.sh - background: true - # Waits for the Saucelabs tunnel to be ready. This ensures that we don't run tests - # too early without Saucelabs not being ready. - - run: ./scripts/saucelabs/wait-for-tunnel.sh - - run: node ./tests/legacy-cli/run_e2e ./tests/legacy-cli/e2e/tests/misc/browsers.ts --ve - - run: node ./tests/legacy-cli/run_e2e ./tests/legacy-cli/e2e/tests/misc/browsers.ts - - run: ./scripts/saucelabs/stop-tunnel.sh - - build: - executor: action-executor - steps: - - custom_attach_workspace - - run: yarn build - - test: - executor: test-executor - resource_class: xlarge - steps: - - custom_attach_workspace - - browser-tools/install-chrome - - setup_bazel_rbe - - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - run: - command: yarn bazel:test - no_output_timeout: 20m - - snapshot_publish: - executor: action-executor - resource_class: medium - steps: - - custom_attach_workspace - - 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 - - # Windows jobs - e2e-cli-win: - executor: windows-executor - parallelism: 8 - steps: - - custom_attach_workspace - - setup_windows - - restore_cache: - keys: - - *cache_key_win - - run: yarn install --frozen-lockfile --cache-folder ../.cache/yarn - - save_cache: - key: *cache_key_win - paths: - - ~/.cache/yarn - # Run partial e2e suite on PRs only. Release branches will run the full e2e suite. - - run: - name: Execute E2E Tests - command: | - if (Test-Path env:CIRCLE_PULL_REQUEST) { - node tests\legacy-cli\run_e2e.js "--glob={tests/basic/**,tests/i18n/extract-ivy*.ts,tests/build/profile.ts}" --nb-shards=$env:CIRCLE_NODE_TOTAL --shard=$env:CIRCLE_NODE_INDEX - } else { - node tests\legacy-cli\run_e2e.js --nb-shards=$env:CIRCLE_NODE_TOTAL --shard=$env:CIRCLE_NODE_INDEX - } - -workflows: - version: 2 - default_workflow: - jobs: - # Linux jobs - - setup - - lint: - requires: - - setup - - validate: - requires: - - setup - - build: - requires: - - setup - - e2e-cli: - name: e2e-cli - post-steps: - - store_artifacts: - path: /tmp/dist - destination: cli/new-production - requires: - - build - - e2e-cli: - name: e2e-cli-ng-snapshots - snapshots: true - requires: - - e2e-cli - filters: - branches: - only: - - renovate/angular - - master - - e2e-cli: - name: e2e-cli-node-12 - nodeversion: '12.20' - <<: *only_release_branches - requires: - - e2e-cli - - test-browsers: - requires: - - build - - # Bazel jobs - # These jobs only really depend on Setup, but the build job is very quick to run (~35s) and - # will catch any build errors before proceeding to the more lengthy and resource intensive - # Bazel jobs. - - test: - requires: - - build - - # Windows jobs - - e2e-cli-win: - requires: - - test - - # Publish jobs - - snapshot_publish: - <<: *only_release_branches - requires: - - build - - test - - e2e-cli diff --git a/.circleci/env-helpers.inc.sh b/.circleci/env-helpers.inc.sh deleted file mode 100644 index 5fa1263e112f..000000000000 --- a/.circleci/env-helpers.inc.sh +++ /dev/null @@ -1,73 +0,0 @@ -#################################################################################################### -# Helpers for defining environment variables for CircleCI. -# -# In CircleCI, each step runs in a new shell. The way to share ENV variables across steps is to -# export them from `$BASH_ENV`, which is automatically sourced at the beginning of every step (for -# the default `bash` shell). -# -# See also https://circleci.com/docs/2.0/env-vars/#using-bash_env-to-set-environment-variables. -#################################################################################################### - -# Set and print an environment variable. -# -# Use this function for setting environment variables that are public, i.e. it is OK for them to be -# visible to anyone through the CI logs. -# -# Usage: `setPublicVar ` -function setPublicVar() { - setSecretVar $1 "$2"; - echo "$1=$2"; -} - -# Set (without printing) an environment variable. -# -# Use this function for setting environment variables that are secret, i.e. should not be visible to -# everyone through the CI logs. -# -# Usage: `setSecretVar ` -function setSecretVar() { - # WARNING: Secrets (e.g. passwords, access tokens) should NOT be printed. - # (Keep original shell options to restore at the end.) - local -r originalShellOptions=$(set +o); - set +x -eu -o pipefail; - - echo "export $1=\"${2:-}\";" >> $BASH_ENV; - - # Restore original shell options. - eval "$originalShellOptions"; -} - - -# Create a function to set an environment variable, when called. -# -# Use this function for creating setter for public environment variables that require expensive or -# time-consuming computaions and may not be needed. When needed, you can call this function to set -# the environment variable (which will be available through `$BASH_ENV` from that point onwards). -# -# Arguments: -# - ``: The name of the environment variable. The generated setter function will be -# `setPublicVar_`. -# - ``: The code to run to compute the value for the variable. Since this code should be -# executed lazily, it must be properly escaped. For example: -# ```sh -# # DO NOT do this: -# createPublicVarSetter MY_VAR "$(whoami)"; # `whoami` will be evaluated eagerly -# -# # DO this isntead: -# createPublicVarSetter MY_VAR "\$(whoami)"; # `whoami` will NOT be evaluated eagerly -# ``` -# -# Usage: `createPublicVarSetter ` -# -# Example: -# ```sh -# createPublicVarSetter MY_VAR 'echo "FOO"'; -# echo $MY_VAR; # Not defined -# -# setPublicVar_MY_VAR; -# source $BASH_ENV; -# echo $MY_VAR; # FOO -# ``` -function createPublicVarSetter() { - echo "setPublicVar_$1() { setPublicVar $1 \"$2\"; }" >> $BASH_ENV; -} diff --git a/.circleci/env.sh b/.circleci/env.sh deleted file mode 100755 index d24334473255..000000000000 --- a/.circleci/env.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -# Variables -readonly projectDir=$(realpath "$(dirname ${BASH_SOURCE[0]})/..") -readonly envHelpersPath="$projectDir/.circleci/env-helpers.inc.sh"; - -# Load helpers and make them available everywhere (through `$BASH_ENV`). -source $envHelpersPath; -echo "source $envHelpersPath;" >> $BASH_ENV; - - -#################################################################################################### -# Define PUBLIC environment variables for CircleCI. -#################################################################################################### -# See https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables for more info. -#################################################################################################### -setPublicVar PROJECT_ROOT "$projectDir"; -setPublicVar NPM_CONFIG_PREFIX "${HOME}/.npm-global"; -setPublicVar PATH "${HOME}/.npm-global/bin:${PATH}"; - -#################################################################################################### -# Define SauceLabs environment variables for CircleCI. -#################################################################################################### -setPublicVar SAUCE_USERNAME "angular-tooling"; -setSecretVar SAUCE_ACCESS_KEY "8c4ffce86ae6-c419-8ef4-0513-54267305"; -setPublicVar SAUCE_LOG_FILE /tmp/angular/sauce-connect.log -setPublicVar SAUCE_READY_FILE /tmp/angular/sauce-connect-ready-file.lock -setPublicVar SAUCE_PID_FILE /tmp/angular/sauce-connect-pid-file.lock -setPublicVar SAUCE_TUNNEL_IDENTIFIER "angular-${CIRCLE_BUILD_NUM}-${CIRCLE_NODE_INDEX}" -# Amount of seconds we wait for sauceconnect to establish a tunnel instance. In order to not -# acquire CircleCI instances for too long if sauceconnect failed, we need a connect timeout. -setPublicVar SAUCE_READY_FILE_TIMEOUT 120 - -# Source `$BASH_ENV` to make the variables available immediately. -source $BASH_ENV; diff --git a/.circleci/gcp_token b/.circleci/gcp_token deleted file mode 100644 index 06773903e8d8..000000000000 Binary files a/.circleci/gcp_token and /dev/null differ 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/.eslintignore b/.eslintignore deleted file mode 100644 index 3a012076c286..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,11 +0,0 @@ -/bazel-out/ -/dist-schema/ -/goldens/public-api -/packages/angular_devkit/build_angular/test/ -/packages/angular_devkit/build_webpack/test/ -/packages/angular_devkit/schematics_cli/schematic/files/ -/tests/ -.yarn/ -dist/ -node_modules/ -third_party/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index c268a567aa33..000000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "root": true, - "env": { - "es6": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:import/typescript", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": ["eslint-plugin-import", "header", "@typescript-eslint"], - "rules": { - "@typescript-eslint/consistent-type-assertions": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-non-null-assertion": "error", - "@typescript-eslint/no-unnecessary-qualifier": "error", - "@typescript-eslint/no-unused-expressions": "error", - "curly": "error", - "header/header": [ - "error", - "block", - [ - "*", - " * @license", - " * Copyright Google LLC All Rights 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", - " " - ], - 2 - ], - "import/first": "error", - "import/newline-after-import": "error", - "import/no-absolute-path": "error", - "import/no-duplicates": "error", - "import/no-extraneous-dependencies": ["error", { "devDependencies": false }], - "import/no-unassigned-import": ["error", { "allow": ["symbol-observable"] }], - "import/order": [ - "error", - { - "alphabetize": { "order": "asc" }, - "groups": [["builtin", "external"], "parent", "sibling", "index"] - } - ], - "max-len": [ - "error", - { - "code": 140, - "ignoreUrls": true - } - ], - "max-lines-per-function": ["error", { "max": 200 }], - "no-caller": "error", - "no-console": "error", - "no-empty": ["error", { "allowEmptyCatch": true }], - "no-eval": "error", - "no-multiple-empty-lines": ["error"], - "no-throw-literal": "error", - "padding-line-between-statements": [ - "error", - { - "blankLine": "always", - "prev": "*", - "next": "return" - } - ], - "sort-imports": ["error", { "ignoreDeclarationSort": true }], - "spaced-comment": [ - "error", - "always", - { - "markers": ["/"] - } - ], - - /* TODO: evaluate usage of these rules and fix issues as needed */ - "no-case-declarations": "off", - "no-fallthrough": "off", - "no-underscore-dangle": "off", - "no-useless-escape": "off", - "@typescript-eslint/await-thenable": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-implied-eval": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-unnecessary-type-assertion": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/prefer-regexp-exec": "off", - "@typescript-eslint/require-await": "off", - "@typescript-eslint/restrict-plus-operands": "off", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/unbound-method": "off" - }, - "overrides": [ - { - "files": ["!packages/**", "**/*_spec.ts"], - "rules": { - "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], - "max-lines-per-function": "off", - "no-console": "off" - } - } - ] -} 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 bc0cec42df0e..000000000000 --- a/.github/ISSUE_TEMPLATE/1-bug-report.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -name: "\U0001F41E Bug report" -about: Report a bug in Angular CLI ---- - - - -# 🞠Bug report - -### Command (mark with an `x`) - - - - -- [ ] new -- [ ] build -- [ ] serve -- [ ] test -- [ ] e2e -- [ ] generate -- [ ] add -- [ ] update -- [ ] lint -- [ ] extract-i18n -- [ ] 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 f129bc107360..000000000000 --- a/.github/ISSUE_TEMPLATE/2-feature-request.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: "\U0001F680 Feature request" -about: Suggest a feature for Angular CLI ---- - - - -# 🚀 Feature request - -### Command (mark with an `x`) - - - - -- [ ] new -- [ ] build -- [ ] serve -- [ ] test -- [ ] e2e -- [ ] generate -- [ ] add -- [ ] update -- [ ] lint -- [ ] extract-i18n -- [ ] 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 7270bb2a963f..000000000000 --- a/.github/ISSUE_TEMPLATE/3-docs-bug.md +++ /dev/null @@ -1,12 +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 a5c2c1707fda..000000000000 --- a/.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md +++ /dev/null @@ -1,10 +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 509f8d4797bc..000000000000 --- a/.github/ISSUE_TEMPLATE/5-support-request.md +++ /dev/null @@ -1,15 +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 8ab207b2389f..000000000000 --- a/.github/ISSUE_TEMPLATE/6-angular-framework.md +++ /dev/null @@ -1,12 +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 10b27db5c86f..000000000000 --- a/.github/ISSUE_TEMPLATE/7-angular-material.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "\U0001F48E Angular 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 466b8ad5ee52..1237bc279e11 100644 --- a/.github/SAVED_REPLIES.md +++ b/.github/SAVED_REPLIES.md @@ -29,7 +29,7 @@ Thanks for reporting this issue. However, this issue is a duplicate of #/external`. + # TODO(devversion): consult with Aspect on why this is needed. + Set-Location -Path "${runfiles_dir}\_main" + New-Item -ItemType SymbolicLink -Path "external" -Target ".." + + - name: Run CLI E2E tests + shell: bash + env: + BAZEL_BINDIR: '.' + E2E_TEMP: ${{ inputs.e2e_temp_dir }} + run: | + node ./scripts/windows-testing/parallel-executor.mjs \ + "./dist/bin/tests/${{ inputs.test_target_name }}_/${{ inputs.test_target_name }}.bat.runfiles" \ + ${{ inputs.test_target_name }} 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..a30031ce31fd --- /dev/null +++ b/.github/workflows/assistant-to-the-branch-manager.yml @@ -0,0 +1,22 @@ +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 + if: github.event.repository.fork == false + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - uses: angular/dev-infra/github-actions/branch-manager@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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..000b239cd7a0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,254 @@ +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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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 run 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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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 -- //... -//tests/... + + e2e: + needs: test + strategy: + fail-fast: false + matrix: + node: [20, 22, 24] + subset: [esbuild, webpack] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + build-e2e-windows: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + with: + google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }} + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build E2E tests for Windows on Linux + run: | + pnpm bazel build \ + --config=e2e \ + //tests:e2e.webpack_node22 \ + //tests:e2e.esbuild_node22 \ + --platforms=tools:windows_x64 + - name: Store built Windows E2E tests + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: win-e2e-build-artifacts + path: | + dist/bin/tests/** + !**/node_modules/** + retention-days: 1 + if-no-files-found: 'error' + + e2e-windows: + needs: build-e2e-windows + strategy: + fail-fast: false + matrix: + node: [22] + subset: [esbuild, webpack] + shard: [0, 1, 2, 3, 4, 5] + runs-on: windows-2025 + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Download built Windows E2E tests + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: win-e2e-build-artifacts + path: dist/bin/tests/ + - 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: + node: [22] + subset: [yarn, pnpm, bun] + shard: [0, 1, 2] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e-snapshots: + needs: test + if: github.ref == 'refs/heads/main' + strategy: + fail-fast: false + matrix: + node: [22] + subset: [esbuild, webpack] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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: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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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:e2e.saucelabs + ./scripts/saucelabs/stop-tunnel.sh + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + 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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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..3932ccf6a3b8 --- /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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + with: + languages: javascript-typescript + build-mode: none + config-file: .github/codeql/config.yml + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + 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..23811dc9f573 --- /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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: angular/dev-infra/github-actions/pull-request-labeling@942d738d8f4d65b161d06e6c399fefec318cdbfe + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} + post_approval_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: angular/dev-infra/github-actions/post-approval-changes@942d738d8f4d65b161d06e6c399fefec318cdbfe + 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..fae7787a6dff --- /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@942d738d8f4d65b161d06e6c399fefec318cdbfe + with: + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/.github/workflows/lock-closed.yml b/.github/workflows/lock-closed.yml deleted file mode 100644 index c8e81beac98a..000000000000 --- a/.github/workflows/lock-closed.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Lock Inactive Issues - -on: - schedule: - # Run at 08:00 every day - - cron: '0 8 * * *' - -jobs: - lock_closed: - runs-on: ubuntu-latest - steps: - - uses: angular/dev-infra/github-actions/lock-closed@4f335a4c1f01f20bf905acee2d68c7248f50f2a0 - with: - lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml new file mode 100644 index 000000000000..abe20d3f3921 --- /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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 + 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..d103d3f2edcd --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,216 @@ +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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + snapshots: + - 'tests/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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup ESLint Caching + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + 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 run 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 + - 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 }} + - name: Check Package Licenses + uses: angular/dev-infra/github-actions/linting/licenses@942d738d8f4d65b161d06e6c399fefec318cdbfe + + build: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + 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@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Run module and package tests + run: pnpm bazel test -- //... -//tests/... + + e2e: + needs: build + strategy: + fail-fast: false + matrix: + node: [22] + subset: [esbuild, webpack] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + build-e2e-windows-subset: + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Build E2E tests for Windows on Linux + run: | + pnpm bazel build \ + --config=e2e \ + //tests:e2e.esbuild_node22 \ + --platforms=tools:windows_x64 + - name: Store built Windows E2E tests + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: win-e2e-build-artifacts + path: | + dist/bin/tests/** + !**/node_modules/** + retention-days: 1 + if-no-files-found: 'error' + + e2e-windows-subset: + needs: build-e2e-windows-subset + runs-on: windows-2025 + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Download built Windows E2E tests + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: win-e2e-build-artifacts + path: dist/bin/tests/ + - name: Run CLI E2E tests + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e.esbuild_node22 + env: + E2E_SHARD_TOTAL: 1 + TESTBRIDGE_TEST_ONLY: tests/basic/{build,rebuild,serve}.ts + + e2e-package-managers: + needs: build + strategy: + fail-fast: false + matrix: + node: [22] + subset: [yarn, pnpm, bun] + shard: [0, 1, 2] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e-snapshots: + needs: [analyze, build] + if: needs.analyze.outputs.snapshots == 'true' + strategy: + fail-fast: false + matrix: + node: [22] + subset: [esbuild, webpack] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ubuntu-latest + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Install node modules + run: pnpm install --frozen-lockfile + - name: Setup Bazel + uses: angular/dev-infra/github-actions/bazel/setup@942d738d8f4d65b161d06e6c399fefec318cdbfe + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@942d738d8f4d65b161d06e6c399fefec318cdbfe + - 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: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..63ad55057037 --- /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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + # Upload the results as artifacts. + - name: 'Upload artifact' + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + 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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 91652321da0e..83582f8ece43 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,14 @@ test-project-host-* dist/ dist-schema/ +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + # IDEs jsconfig.json @@ -23,7 +31,7 @@ jsconfig.json # VSCode # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore -.vscode/ +.vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec13899..000000000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/commit-msg b/.husky/commit-msg index 1b07f649c828..0c6213fc6bb7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname $0)/_/husky.sh" - -yarn -s ng-dev commit-message pre-commit-validate --file $1; +pnpm -s ng-dev commit-message pre-commit-validate --file $1; diff --git a/.husky/pre-commit b/.husky/pre-commit index 84611a58eec9..bbcdc40e0112 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname $0)/_/husky.sh" - -yarn -s ng-dev format staged; \ No newline at end of file +pnpm -s ng-dev format staged; diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index 3a3afe6f32f5..2333b7b798c0 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname $0)/_/husky.sh" - -yarn -s ng-dev commit-message restore-commit-message-draft $1 $2; +pnpm -s ng-dev commit-message restore-commit-message-draft $1 $2; diff --git a/.idea/angular-cli.iml b/.idea/angular-cli.iml deleted file mode 100644 index cff4053c5974..000000000000 --- a/.idea/angular-cli.iml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 28a804d8932a..000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 6d8c965387b0..000000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/runConfigurations/Large_Tests.xml b/.idea/runConfigurations/Large_Tests.xml deleted file mode 100644 index 3d4f25fb3a76..000000000000 --- a/.idea/runConfigurations/Large_Tests.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - ', + ); + 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..91c4cafc571a --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/loader-import-attribute_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: "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 inline base64 content for file extension set to "base64"', 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: "base64" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the base64 encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('QUJD'); + harness.expectFile('dist/browser/main.js').content.not.toContain('ABC'); + }); + + it('should inline dataurl content for file extension set to "dataurl"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + await harness.writeFile('./src/a.svg', 'ABC'); + await harness.writeFile( + 'src/main.ts', + '// @ts-expect-error\nimport contents from "./a.svg" with { loader: "dataurl" };\n console.log(contents);', + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + // Should contain the dataurl encoding used esbuild and not the text content + harness.expectFile('dist/browser/main.js').content.toContain('data:image/svg+xml,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..7bfcca94d242 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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: "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, + }); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('foo'); + + await harness.writeFile('public/asset.txt', 'bar'); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset.txt').content.toContain('bar'); + }, + ]); + }); + + 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, + }); + + await harness.executeWithCases([ + async ({ result }) => { + 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'); + }, + + ({ result }) => { + expect(result?.success).toBeTrue(); + harness.expectFile('dist/browser/asset-one.txt').toExist(); + harness.expectFile('dist/browser/asset-two.txt').toNotExist(); + }, + ]); + }); + }); +}); 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..26ae35a8221f --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-component_styles_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'; + +/** + * 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 './a';"); + await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBe(true); + + 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; }'); + }, + async ({ result }) => { + expect(result?.success).toBe(true); + + 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; }'); + }, + ({ result }) => { + expect(result?.success).toBe(true); + + 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'); + }, + ]); + }); + } + }); +}); 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..fa384be88080 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-errors_spec.ts @@ -0,0 +1,247 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, + expectLog, + expectNoLog, +} 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 { } + `, + ); + + await harness.executeWithCases( + [ + async ({ result }) => { + 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; + } + `, + ); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expectLog(logs, 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'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expectLog(logs, typeErrorText); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expectNoLog(logs, 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'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expectNoLog(logs, typeErrorText); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + it('detects cumulative block syntax errors', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.executeWithCases( + [ + async () => { + // Add invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@if-one'); + }, + async ({ logs }) => { + expectLog(logs, '@if-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'); + }, + async ({ logs }) => { + expectLog(logs, '@if-one'); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@if-two'); + }, + async ({ logs }) => { + expectLog(logs, '@if-one'); + expectLog(logs, '@if-two'); + + // Add more invalid block syntax + await harness.appendToFile('src/app/app.component.html', '@if-three'); + }, + async ({ logs }) => { + expectLog(logs, '@if-one'); + expectLog(logs, '@if-two'); + expectLog(logs, '@if-three'); + + // Revert the changes that caused the error + // Should remove the error + await harness.writeFile('src/app/app.component.html', '

GOOD

'); + }, + ({ logs }) => { + expectNoLog(logs, '@if-one'); + expectNoLog(logs, '@if-two'); + expectNoLog(logs, '@if-three'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + it('recovers from component stylesheet error', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + aot: false, + }); + + await harness.executeWithCases( + [ + async () => { + await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); + }, + async ({ logs }) => { + expectLog(logs, 'invalid-css-content'); + + await harness.writeFile('src/app/app.component.css', 'p { color: green }'); + }, + ({ logs }) => { + expectNoLog(logs, 'invalid-css-content'); + + harness + .expectFile('dist/browser/main.js') + .content.toContain('p {\\n color: green;\\n}'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + it('recovers from component template error', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.executeWithCases( + [ + async () => { + // Missing ending `>` on the div will cause an error + await harness.appendToFile('src/app/app.component.html', '
Hello, world! { + expectLog(logs, 'Unexpected character "EOF"'); + + await harness.appendToFile('src/app/app.component.html', '>'); + }, + async ({ logs }) => { + expectNoLog(logs, '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
'); + }, + ({ logs }) => { + expectNoLog(logs, 'invalid-css-content'); + + harness.expectFile('dist/browser/main.js').content.toContain('Hello, world!'); + harness.expectFile('dist/browser/main.js').content.toContain('Guten Tag'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + }); +}); 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..d9ea8870f687 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-general_spec.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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'; + +/** + * 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 { } + `, + ); + + await harness.executeWithCases( + [ + async ({ result }) => { + 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'); + }, + async ({ result }) => { + // 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';`, ''), + ); + }, + async ({ result }) => { + 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, + ); + }, + async ({ result }) => { + 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'), + ); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/main.js').content.toContain('FILE-B'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + }); +}); 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..22c4c32202bd --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-global_styles_spec.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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: "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 './a';"); + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + + await harness.executeWithCases( + [ + async ({ result }) => { + 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; }', + ); + }, + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: blue;\\nh1 { color: $primary; }'); + }, + ({ result }) => { + 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'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + 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 './a';"); + await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + }, + async ({ result }) => { + 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; }'); + }, + ({ result }) => { + 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'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + + 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 './a';"); + await harness.writeFile('src/other.scss', "@import './a'; h1 { color: green; }"); + await harness.writeFile('src/a.scss', 'invalid-invalid-invalid\\nh1 { color: $primary; }'); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBe(false); + + await harness.writeFile('src/a.scss', '$primary: aqua;\\nh1 { color: $primary; }'); + }, + async ({ result }) => { + 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; }'); + }, + ({ result }) => { + 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'); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + }); +}); 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..99603bc98cee --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-index-html_spec.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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'; + +/** + * 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, + }); + + await harness.executeWithCases([ + async ({ result }) => { + 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'), + ); + }, + async ({ result }) => { + 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"'), + ); + }, + ({ result }) => { + expect(result?.success).toBe(true); + harness.expectFile('dist/browser/index.html').content.toContain('charset="utf-8"'); + }, + ]); + }); + }); +}); 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..2fdad10f8d8d --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/rebuild-web-workers_spec.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, + expectLog, + expectNoLog, +} 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: "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' }); + } + `, + ); + + await harness.executeWithCases( + [ + async ({ result }) => { + 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`); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expectLog(logs, 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'); + }, + async ({ logs }) => { + expectLog(logs, errorText); + + // Revert the change that caused the error + // Should remove the error + await harness.writeFile('src/app/worker.ts', workerCodeFile); + }, + async ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expectNoLog(logs, 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'); + }, + ({ result, logs }) => { + expect(result?.success).toBeTrue(); + expectNoLog(logs, errorText); + + // Ensure built worker is referenced in the application code + harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + }); +}); 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("~/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 "a.css"; + `, + ); + await harness.writeFile( + 'src/a.css', + ` + .a { + background-image: url("~/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 "a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("~/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("^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 "a"; + `, + ); + await harness.writeFile( + 'src/a.scss', + ` + .a { + background-image: url("^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(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 './b'; + .a { + background-image: url($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(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("./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(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("./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(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("./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 './b'; + .a { + background-image: url($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("./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 './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("./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 './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("./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 './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("./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 './b'; + .a { + $asset: my-function-url('logo'); + background-image: url($asset) + } + `, + 'src/theme/b.scss': `@function my-function-url($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("./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("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(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(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..ba01e2a27dce --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-lazy_spec.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 { buildApplication } from '../../index'; +import { OutputHashing } from '../../schema'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder, expectLog } 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, + }); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Add valid code + await harness.appendToFile('src/lazy.ts', `console.log('foo');`); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Update type of 'foo' to invalid (number -> string) + await harness.writeFile('src/lazy.ts', `export const foo: string = 1;`); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expectLog(logs, `Type 'number' is not assignable to type 'string'.`); + + // Fix TS error + await harness.writeFile('src/lazy.ts', `export const foo: string = "1";`); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + }); +}); 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..eeb160ebef47 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-rebuild-touch-file_spec.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 { 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, + }); + + await harness.executeWithCases( + [ + async ({ result }) => { + expect(result?.success).toBeTrue(); + // Touch a file without doing any changes. + await harness.modifyFile('src/app/app.component.ts', (content) => content); + }, + async ({ result }) => { + expect(result?.success).toBeTrue(); + await harness.removeFile('src/app/app.component.ts'); + }, + ({ result }) => { + expect(result?.success).toBeFalse(); + }, + ], + { outputLogsOnFailure: false }, + ); + }); + } + }); +}); 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..cf21d5545f7a --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/typescript-resolve-json_spec.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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.moduleResolution = 'node'; + 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.moduleResolution = 'node'; + 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.moduleResolution = 'node'; + 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..bcc361ccdbe1 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/allowed-common-js-dependencies_spec.ts @@ -0,0 +1,178 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, + expectLog, + expectNoLog, +} 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); + expectLog(logs, /Module 'buffer' used by 'src\/app\/app\.component\.ts' is not ESM/); + expectLog(logs, /CommonJS or AMD dependencies/); + expectNoLog( + logs, + '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); + expectNoLog(logs, /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); + expectNoLog(logs, /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); + expectNoLog(logs, /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); + expectNoLog(logs, /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); + expectNoLog(logs, /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); + expectNoLog(logs, /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(' + + +
+

Blocked request. This host ("${hostname}") is not allowed.

+

To allow this host, add it to allowedHosts under the serve target in angular.json.

+
{
+  "serve": {
+    "options": {
+      "allowedHosts": ["${hostname}"]
+    }
+  }
+}
+
+ + `; +} diff --git a/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.ts new file mode 100644 index 000000000000..cd52b8a7904f --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/html-fallback-middleware.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 type { ServerResponse } from 'node:http'; +import type { Connect } from 'vite'; +import { lookupMimeTypeFromRequest } from '../utils'; + +const ALLOWED_FALLBACK_METHODS = Object.freeze(['GET', 'HEAD']); + +export function angularHtmlFallbackMiddleware( + req: Connect.IncomingMessage, + _res: ServerResponse, + next: Connect.NextFunction, +): void { + // Similar to how it is handled in vite + // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45 + if (!req.method || !ALLOWED_FALLBACK_METHODS.includes(req.method)) { + // No fallback for unsupported request methods + next(); + + return; + } + + if (req.url) { + const mimeType = lookupMimeTypeFromRequest(req.url); + if ( + (mimeType === 'text/html' || mimeType === 'application/xhtml+xml') && + !/^\/index\.(?:csr\.)?html/.test(req.url) + ) { + // eslint-disable-next-line no-console + console.warn( + `Request for HTML file "${req.url}" was received but no asset found. Asset may be missing from build.`, + ); + } else if (mimeType) { + // No fallback for request of asset-like files + next(); + + return; + } + } + + if ( + !req.headers.accept || + req.headers.accept.includes('text/html') || + req.headers.accept.includes('text/*') || + req.headers.accept.includes('*/*') + ) { + req.url = '/index.html'; + } + + next(); +} diff --git a/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.ts new file mode 100644 index 000000000000..7959ccb7ec03 --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/index-html-middleware.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 { extname } from 'node:path'; +import type { Connect, ViteDevServer } from 'vite'; +import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils'; + +export function createAngularIndexHtmlMiddleware( + server: ViteDevServer, + outputFiles: AngularMemoryOutputFiles, + resetComponentUpdates: () => void, + indexHtmlTransformer: ((content: string) => Promise) | undefined, +): Connect.NextHandleFunction { + return function angularIndexHtmlMiddleware(req, res, next) { + if (!req.url) { + next(); + + return; + } + + // Parse the incoming request. + // The base of the URL is unused but required to parse the URL. + const pathname = pathnameWithoutBasePath(req.url, server.config.base); + const extension = extname(pathname); + if (extension !== '.html') { + next(); + + return; + } + + const rawHtml = outputFiles.get(pathname)?.contents; + if (!rawHtml) { + next(); + + return; + } + + // A request for the index indicates a full page reload request. + resetComponentUpdates(); + + server + .transformIndexHtml(req.url, Buffer.from(rawHtml).toString('utf-8')) + .then(async (processedHtml) => { + if (indexHtmlTransformer) { + processedHtml = await indexHtmlTransformer(processedHtml); + } + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(processedHtml); + }) + .catch((error) => next(error)); + }; +} diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts new file mode 100644 index 000000000000..807e739eed59 --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export { type ComponentStyleRecord, createAngularAssetsMiddleware } from './assets-middleware'; +export { angularHtmlFallbackMiddleware } from './html-fallback-middleware'; +export { createAngularIndexHtmlMiddleware } from './index-html-middleware'; +export { + createAngularSsrExternalMiddleware, + createAngularSsrInternalMiddleware, +} from './ssr-middleware'; +export { createAngularHeadersMiddleware } from './headers-middleware'; +export { createAngularComponentMiddleware } from './component-middleware'; +export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware'; +export { patchHostValidationMiddleware } from './host-check-middleware'; +export { patchBaseMiddleware } from './base-middleware'; diff --git a/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts new file mode 100644 index 000000000000..4b0a8d8390f1 --- /dev/null +++ b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts @@ -0,0 +1,151 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { + AngularAppEngine as SSRAngularAppEngine, + ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp, +} from '@angular/ssr'; +import type { ServerResponse } from 'node:http'; +import type { Connect, ViteDevServer } from 'vite'; +import { + isSsrNodeRequestHandler, + isSsrRequestHandler, +} from '../../../utils/server-rendering/utils'; + +export function createAngularSsrInternalMiddleware( + server: ViteDevServer, + indexHtmlTransformer?: (content: string) => Promise, +): Connect.NextHandleFunction { + let cachedAngularServerApp: ReturnType | undefined; + + return function angularSsrMiddleware( + req: Connect.IncomingMessage, + res: ServerResponse, + next: Connect.NextFunction, + ) { + if (req.url === undefined) { + return next(); + } + + (async () => { + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } = (await import( + '@angular/ssr/node' as string + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); + + const { ɵgetOrCreateAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as { + ɵgetOrCreateAngularServerApp: typeof getOrCreateAngularServerApp; + }; + + const angularServerApp = ɵgetOrCreateAngularServerApp({ + allowStaticRouteRender: true, + }); + + // Only Add the transform hook only if it's a different instance. + if (cachedAngularServerApp !== angularServerApp) { + angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => { + const processedHtml = await server.transformIndexHtml(url.pathname, html); + + return indexHtmlTransformer?.(processedHtml) ?? processedHtml; + }); + + cachedAngularServerApp = angularServerApp; + } + + const webReq = new Request(createWebRequestFromNodeRequest(req), { + signal: AbortSignal.timeout(30_000), + }); + const webRes = await angularServerApp.handle(webReq); + if (!webRes) { + return next(); + } + + return writeResponseToNodeResponse(webRes, res); + })().catch(next); + }; +} + +export async function createAngularSsrExternalMiddleware( + server: ViteDevServer, + indexHtmlTransformer?: (content: string) => Promise, +): Promise { + let fallbackWarningShown = false; + let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined; + let angularSsrInternalMiddleware: + | ReturnType + | undefined; + + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + + const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = (await import( + '@angular/ssr/node' as string + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); + + return function angularSsrExternalMiddleware( + req: Connect.IncomingMessage, + res: ServerResponse, + next: Connect.NextFunction, + ) { + (async () => { + const { reqHandler, AngularAppEngine } = (await server.ssrLoadModule('./server.mjs')) as { + reqHandler?: unknown; + AngularAppEngine: typeof SSRAngularAppEngine; + }; + + if (!isSsrNodeRequestHandler(reqHandler) && !isSsrRequestHandler(reqHandler)) { + if (!fallbackWarningShown) { + // eslint-disable-next-line no-console + console.warn( + `The 'reqHandler' export in 'server.ts' is either undefined or does not provide a recognized request handler. ` + + 'Using the internal SSR middleware instead.', + ); + + fallbackWarningShown = true; + } + + angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware( + server, + indexHtmlTransformer, + ); + + angularSsrInternalMiddleware(req, res, next); + + return; + } + + if (cachedAngularAppEngine !== AngularAppEngine) { + AngularAppEngine.ɵallowStaticRouteRender = true; + AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => { + const processedHtml = await server.transformIndexHtml(url.pathname, html); + + return indexHtmlTransformer?.(processedHtml) ?? processedHtml; + }); + + cachedAngularAppEngine = AngularAppEngine; + } + + // Forward the request to the middleware in server.ts + if (isSsrNodeRequestHandler(reqHandler)) { + await reqHandler(req, res, next); + } else { + const webRes = await reqHandler(createWebRequestFromNodeRequest(req)); + if (!webRes) { + next(); + + return; + } + + await writeResponseToNodeResponse(webRes, res); + } + })().catch(next); + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts new file mode 100644 index 000000000000..be00e3437f27 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -0,0 +1,151 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { readFile } from 'node:fs/promises'; +import { dirname, join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { Plugin } from 'vite'; +import { AngularMemoryOutputFiles } from '../utils'; + +interface AngularMemoryPluginOptions { + virtualProjectRoot: string; + outputFiles: AngularMemoryOutputFiles; + templateUpdates?: ReadonlyMap; + external?: string[]; + disableViteTransport?: boolean; +} + +const ANGULAR_PREFIX = '/@ng/'; +const VITE_FS_PREFIX = '/@fs/'; +const FILE_PROTOCOL = 'file:'; + +export async function createAngularMemoryPlugin( + options: AngularMemoryPluginOptions, +): Promise { + const { virtualProjectRoot, outputFiles, external } = options; + const { normalizePath } = await import('vite'); + + return { + name: 'vite:angular-memory', + // Ensures plugin hooks run before built-in Vite hooks + enforce: 'pre', + async resolveId(source, importer, { ssr }) { + if (source.startsWith(VITE_FS_PREFIX)) { + return; + } + + // For SSR with component HMR, pass through as a virtual module + if (ssr && source.startsWith(FILE_PROTOCOL) && source.includes(ANGULAR_PREFIX)) { + // Vite will resolve these these files example: + // `file:///@ng/component?c=src%2Fapp%2Fapp.component.ts%40AppComponent&t=1737017253850` + const sourcePath = fileURLToPath(source); + const sourceWithoutRoot = normalizePath('/' + relative(virtualProjectRoot, sourcePath)); + + if (sourceWithoutRoot.startsWith(ANGULAR_PREFIX)) { + const [, query] = source.split('?', 2); + + return `\0${sourceWithoutRoot}?${query}`; + } + } + + // Prevent vite from resolving an explicit external dependency (`externalDependencies` option) + if (external?.includes(source)) { + // This is still not ideal since Vite will still transform the import specifier to + // `/@id/${source}` but is currently closer to a raw external than a resolved file path. + return source; + } + + if (importer && source[0] === '.') { + const normalizedImporter = normalizePath(importer); + if (normalizedImporter.startsWith(virtualProjectRoot)) { + // Remove query if present + const [importerFile] = normalizedImporter.split('?', 1); + source = '/' + join(dirname(relative(virtualProjectRoot, importerFile)), source); + } + } + + const [file] = source.split('?', 1); + if (outputFiles.has(normalizePath(file))) { + return join(virtualProjectRoot, source); + } + }, + load(id, loadOptions) { + // For SSR component updates, return the component update module or empty if none + if (loadOptions?.ssr && id.startsWith(`\0${ANGULAR_PREFIX}`)) { + // Extract component identifier (first character is rollup virtual module null) + const requestUrl = new URL(id.slice(1), 'http://localhost'); + const componentId = requestUrl.searchParams.get('c'); + + return (componentId && options.templateUpdates?.get(encodeURIComponent(componentId))) ?? ''; + } + + const [file] = id.split('?', 1); + const relativeFile = '/' + normalizePath(relative(virtualProjectRoot, file)); + const codeContents = outputFiles.get(relativeFile)?.contents; + if (codeContents === undefined) { + if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) { + return loadViteClientCode(file, options.disableViteTransport); + } + + return undefined; + } + + const code = Buffer.from(codeContents).toString('utf-8'); + const mapContents = outputFiles.get(relativeFile + '.map')?.contents; + + return { + // Remove source map URL comments from the code if a sourcemap is present. + // Vite will inline and add an additional sourcemap URL for the sourcemap. + code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code, + map: mapContents && Buffer.from(mapContents).toString('utf-8'), + }; + }, + }; +} + +/** + * Reads the resolved Vite client code from disk and updates the content to remove + * an unactionable suggestion to update the Vite configuration file to disable the + * error overlay. The Vite configuration file is not present when used in the Angular + * CLI. + * @param file The absolute path to the Vite client code. + * @returns + */ +async function loadViteClientCode(file: string, disableViteTransport = false): Promise { + const originalContents = await readFile(file, 'utf-8'); + let updatedContents = originalContents.replace( + '"You can also disable this overlay by setting ", ' + + 'h("code", { part: "config-option-name" }, "server.hmr.overlay"), ' + + '" to ", ' + + 'h("code", { part: "config-option-value" }, "false"), ' + + '" in ", ' + + 'h("code", { part: "config-file-name" }, hmrConfigName), ' + + '"."', + '', + ); + + assert(originalContents !== updatedContents, 'Failed to update Vite client error overlay text.'); + + if (disableViteTransport) { + const previousUpdatedContents = updatedContents; + + updatedContents = updatedContents.replace( + 'transport.connect(createHMRHandler(handleMessage));', + '', + ); + assert( + previousUpdatedContents !== updatedContents, + 'Failed to update Vite client WebSocket disable.', + ); + + updatedContents = updatedContents.replace('console.debug("[vite] connecting...")', ''); + } + + return updatedContents; +} diff --git a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts new file mode 100644 index 000000000000..5e543734b863 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { Plugin } from 'vite'; + +// NOTE: the implementation for this Vite plugin is roughly based on: +// https://github.com/MilanKovacic/vite-plugin-externalize-dependencies + +const VITE_ID_PREFIX = '@id/'; + +const escapeRegexSpecialChars = (inputString: string): string => { + return inputString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +}; + +export function createRemoveIdPrefixPlugin(externals: string[]): Plugin { + return { + name: 'angular-plugin-remove-id-prefix', + apply: 'serve', + configResolved: (resolvedConfig) => { + // don't do anything when the list of externals is empty + if (externals.length === 0) { + return; + } + + const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?'); + const prefixedExternalRegex = new RegExp( + `${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`, + 'g', + ); + + // @ts-expect-error: Property 'push' does not exist on type 'readonly Plugin[]' + // Reasoning: + // since the /@id/ prefix is added by Vite's import-analysis plugin, + // we must add our actual plugin dynamically, to ensure that it will run + // AFTER the import-analysis. + resolvedConfig.plugins.push({ + name: 'angular-plugin-remove-id-prefix-transform', + transform: (code: string) => { + // don't do anything when code does not contain the Vite prefix + if (!code.includes(VITE_ID_PREFIX)) { + return code; + } + + return code.replace(prefixedExternalRegex, (_, externalName) => externalName); + }, + }); + }, + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/index.ts b/packages/angular/build/src/tools/vite/plugins/index.ts new file mode 100644 index 000000000000..6c4cdd4496e4 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/index.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 + */ + +export { createAngularMemoryPlugin } from './angular-memory-plugin'; +export { createRemoveIdPrefixPlugin } from './id-prefix-plugin'; +export { createAngularSetupMiddlewaresPlugin, ServerSsrMode } from './setup-middlewares-plugin'; +export { createAngularSsrTransformPlugin } from './ssr-transform-plugin'; +export { createAngularServerSideSSLPlugin } from './ssr-ssl-plugin'; diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts new file mode 100644 index 000000000000..5d20d5c705ac --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.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 type { Connect, Plugin } from 'vite'; +import { + ComponentStyleRecord, + angularHtmlFallbackMiddleware, + createAngularAssetsMiddleware, + createAngularComponentMiddleware, + createAngularHeadersMiddleware, + createAngularIndexHtmlMiddleware, + createAngularSsrExternalMiddleware, + createAngularSsrInternalMiddleware, + createChromeDevtoolsMiddleware, + patchBaseMiddleware, + patchHostValidationMiddleware, +} from '../middlewares'; +import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils'; + +export enum ServerSsrMode { + /** + * No SSR + */ + NoSsr, + + /** + * Internal server-side rendering (SSR) is handled through the built-in middleware. + * + * In this mode, the SSR process is managed internally by the dev-server's middleware. + * The server automatically renders pages on the server without requiring external + * middleware or additional configuration from the developer. + */ + InternalSsrMiddleware, + + /** + * External server-side rendering (SSR) is handled by a custom middleware defined in server.ts. + * + * This mode allows developers to define custom SSR behavior by providing a middleware in the + * `server.ts` file. It gives more flexibility for handling SSR, such as integrating with other + * frameworks or customizing the rendering pipeline. + */ + ExternalSsrMiddleware, +} + +interface AngularSetupMiddlewaresPluginOptions { + outputFiles: AngularMemoryOutputFiles; + assets: AngularOutputAssets; + extensionMiddleware?: Connect.NextHandleFunction[]; + indexHtmlTransformer?: (content: string) => Promise; + componentStyles: Map; + templateUpdates: Map; + ssrMode: ServerSsrMode; + resetComponentUpdates: () => void; + projectRoot: string; +} + +async function createEncapsulateStyle(): Promise< + (style: Uint8Array, componentId: string) => string +> { + const { encapsulateStyle } = await import('@angular/compiler'); + const decoder = new TextDecoder('utf-8'); + + return (style, componentId) => { + return encapsulateStyle(decoder.decode(style), componentId); + }; +} + +export function createAngularSetupMiddlewaresPlugin( + options: AngularSetupMiddlewaresPluginOptions, +): Plugin { + return { + name: 'vite:angular-setup-middlewares', + enforce: 'pre', + async configureServer(server) { + const { + indexHtmlTransformer, + outputFiles, + extensionMiddleware, + assets, + componentStyles, + templateUpdates, + ssrMode, + resetComponentUpdates, + } = options; + + const middlewares = server.middlewares; + + // Headers, assets and resources get handled first + middlewares.use(createAngularHeadersMiddleware(server)); + middlewares.use(createAngularComponentMiddleware(server, templateUpdates)); + middlewares.use( + createAngularAssetsMiddleware( + server, + assets, + outputFiles, + componentStyles, + await createEncapsulateStyle(), + ), + ); + + middlewares.use(createChromeDevtoolsMiddleware(server.config.cacheDir, options.projectRoot)); + + extensionMiddleware?.forEach((middleware) => middlewares.use(middleware)); + + // Returning a function, installs middleware after the main transform middleware but + // before the built-in HTML middleware + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return async () => { + patchHostValidationMiddleware(server.middlewares); + + if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) { + patchBaseMiddleware(server.middlewares, server.config.base); + middlewares.use(await createAngularSsrExternalMiddleware(server, indexHtmlTransformer)); + + return; + } + + if (ssrMode === ServerSsrMode.InternalSsrMiddleware) { + middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer)); + } + + middlewares.use(angularHtmlFallbackMiddleware); + middlewares.use( + createAngularIndexHtmlMiddleware( + server, + outputFiles, + resetComponentUpdates, + indexHtmlTransformer, + ), + ); + }; + }, + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.ts new file mode 100644 index 000000000000..80ddf56e739a --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/ssr-ssl-plugin.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 { readFile } from 'node:fs/promises'; +import { getCACertificates, rootCertificates, setDefaultCACertificates } from 'node:tls'; +import type { Plugin } from 'vite'; + +export function createAngularServerSideSSLPlugin(): Plugin { + return { + name: 'angular-ssr-ssl-plugin', + apply: 'serve', + async configureServer({ config, httpServer }) { + const { + ssr, + server: { https }, + } = config; + + if (!ssr || !https?.cert) { + return; + } + + if (httpServer && 'ALPNProtocols' in httpServer) { + // Force Vite to use HTTP/1.1 when SSR and SSL are enabled. + // This is required because the Express server used for SSR does not support HTTP/2. + // See: https://github.com/vitejs/vite/blob/46d3077f2b63771cc50230bc907c48f5773c00fb/packages/vite/src/node/http.ts#L126 + + // We directly set the `ALPNProtocols` on the HTTP server to override the default behavior. + // Passing `ALPNProtocols` in the TLS options would cause Node.js to automatically include `h2`. + // Additionally, using `ALPNCallback` is not an option as it is mutually exclusive with `ALPNProtocols`. + // See: https://github.com/nodejs/node/blob/b8b4350ed3b73d225eb9e628d69151df56eaf298/lib/internal/http2/core.js#L3351 + httpServer.ALPNProtocols = ['http/1.1']; + } + + const { cert } = https; + const additionalCerts = Array.isArray(cert) ? cert : [cert]; + + // TODO(alanagius): Remove the `if` check once we only support Node.js 22.18.0+ and 24.5.0+. + if (getCACertificates && setDefaultCACertificates) { + const currentCerts = getCACertificates('default'); + setDefaultCACertificates([...currentCerts, ...additionalCerts]); + + return; + } + + // TODO(alanagius): Remove the below and `undici` dependency once we only support Node.js 22.18.0+ and 24.5.0+. + const { getGlobalDispatcher, setGlobalDispatcher, Agent } = await import('undici'); + const originalDispatcher = getGlobalDispatcher(); + const ca = [...rootCertificates, ...additionalCerts]; + const extraNodeCerts = process.env['NODE_EXTRA_CA_CERTS']; + if (extraNodeCerts) { + ca.push(await readFile(extraNodeCerts)); + } + + setGlobalDispatcher( + new Agent({ + connect: { + ca, + }, + }), + ); + + httpServer?.on('close', () => { + setGlobalDispatcher(originalDispatcher); + }); + }, + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts b/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts new file mode 100644 index 000000000000..90d183acde02 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/ssr-transform-plugin.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import remapping, { SourceMapInput } from '@ampproject/remapping'; +import type { Plugin } from 'vite'; + +export async function createAngularSsrTransformPlugin(workspaceRoot: string): Promise { + const { normalizePath } = await import('vite'); + + return { + name: 'vite:angular-ssr-transform', + enforce: 'post', + transform(code, _id, { ssr, inMap }: { ssr?: boolean; inMap?: SourceMapInput } = {}) { + if (!ssr || !inMap) { + return null; + } + + const remappedMap = remapping([inMap], () => null); + // Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root. + remappedMap.sourceRoot = normalizePath(workspaceRoot) + '/'; + + return { + code, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + map: remappedMap as any, + }; + }, + }; +} diff --git a/packages/angular/build/src/tools/vite/utils.ts b/packages/angular/build/src/tools/vite/utils.ts new file mode 100644 index 000000000000..2f7cfba84306 --- /dev/null +++ b/packages/angular/build/src/tools/vite/utils.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 { lookup as lookupMimeType } from 'mrmime'; +import { builtinModules, isBuiltin } from 'node:module'; +import { extname } from 'node:path'; +import type { DepOptimizationConfig } from 'vite'; +import type { ExternalResultMetadata } from '../esbuild/bundler-execution-result'; +import { JavaScriptTransformer } from '../esbuild/javascript-transformer'; +import { getFeatureSupport } from '../esbuild/utils'; + +export type AngularMemoryOutputFiles = Map< + string, + { contents: Uint8Array; hash: string; servable: boolean } +>; + +export type AngularOutputAssets = Map; + +export function pathnameWithoutBasePath(url: string, basePath: string): string { + const parsedUrl = new URL(url, 'http://localhost'); + const pathname = decodeURIComponent(parsedUrl.pathname); + + // slice(basePath.length - 1) to retain the trailing slash + return basePath !== '/' && pathname.startsWith(basePath) + ? pathname.slice(basePath.length - 1) + : pathname; +} + +export function lookupMimeTypeFromRequest(url: string): string | undefined { + const extension = extname(url.split('?')[0]); + + if (extension === '.ico') { + return 'image/x-icon'; + } + + return extension && lookupMimeType(extension); +} + +type ViteEsBuildPlugin = NonNullable< + NonNullable['plugins'] +>[0]; + +export type EsbuildLoaderOption = Exclude< + DepOptimizationConfig['esbuildOptions'], + undefined +>['loader']; + +export function getDepOptimizationConfig({ + disabled, + exclude, + include, + target, + zoneless, + prebundleTransformer, + ssr, + loader, + thirdPartySourcemaps, + define = {}, +}: { + disabled: boolean; + exclude: string[]; + include: string[]; + target: string[]; + prebundleTransformer: JavaScriptTransformer; + ssr: boolean; + zoneless: boolean; + loader?: EsbuildLoaderOption; + thirdPartySourcemaps: boolean; + define: Record | undefined; +}): DepOptimizationConfig { + const plugins: ViteEsBuildPlugin[] = [ + { + name: `angular-vite-optimize-deps${ssr ? '-ssr' : ''}${ + thirdPartySourcemaps ? '-vendor-sourcemap' : '' + }`, + setup(build) { + build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => { + return { + contents: await prebundleTransformer.transformFile(args.path), + loader: 'js', + }; + }); + }, + }, + ]; + + return { + // Exclude any explicitly defined dependencies (currently build defined externals) + exclude, + // NB: to disable the deps optimizer, set optimizeDeps.noDiscovery to true and optimizeDeps.include as undefined. + // Include all implict dependencies from the external packages internal option + include: disabled ? undefined : include, + noDiscovery: disabled, + // Add an esbuild plugin to run the Angular linker on dependencies + esbuildOptions: { + // Set esbuild supported targets. + target, + supported: getFeatureSupport(target, zoneless), + plugins, + loader, + define: { + ...define, + 'ngServerMode': `${ssr}`, + }, + resolveExtensions: ['.mjs', '.js', '.cjs'], + }, + }; +} + +export interface DevServerExternalResultMetadata { + implicitBrowser: string[]; + implicitServer: string[]; + explicitBrowser: string[]; + explicitServer: string[]; +} + +export function isAbsoluteUrl(url: string): boolean { + try { + new URL(url); + + return true; + } catch { + return false; + } +} + +export function updateExternalMetadata( + result: { detail?: { externalMetadata?: ExternalResultMetadata } }, + externalMetadata: DevServerExternalResultMetadata, + externalDependencies: string[] | undefined, + explicitPackagesOnly: boolean = false, +): void { + if (!result.detail?.['externalMetadata']) { + return; + } + + const { implicitBrowser, implicitServer, explicit } = result.detail['externalMetadata']; + const implicitServerFiltered = implicitServer.filter((m) => !isBuiltin(m) && !isAbsoluteUrl(m)); + const implicitBrowserFiltered = implicitBrowser.filter((m) => !isAbsoluteUrl(m)); + const explicitBrowserFiltered = explicitPackagesOnly + ? explicit.filter((m) => !isAbsoluteUrl(m)) + : explicit; + + // Empty Arrays to avoid growing unlimited with every re-build. + externalMetadata.explicitBrowser.length = 0; + externalMetadata.explicitServer.length = 0; + externalMetadata.implicitServer.length = 0; + externalMetadata.implicitBrowser.length = 0; + + const externalDeps = externalDependencies ?? []; + externalMetadata.explicitBrowser.push(...explicitBrowserFiltered, ...externalDeps); + externalMetadata.explicitServer.push( + ...explicitBrowserFiltered, + ...externalDeps, + ...builtinModules, + ); + externalMetadata.implicitServer.push(...implicitServerFiltered); + externalMetadata.implicitBrowser.push(...implicitBrowserFiltered); + + // The below needs to be sorted as Vite uses these options as part of the hashing invalidation algorithm. + // See: https://github.com/vitejs/vite/blob/0873bae0cfe0f0718ad2f5743dd34a17e4ab563d/packages/vite/src/node/optimizer/index.ts#L1203-L1239 + externalMetadata.explicitBrowser.sort(); + externalMetadata.explicitServer.sort(); + externalMetadata.implicitServer.sort(); + externalMetadata.implicitBrowser.sort(); +} diff --git a/packages/angular/build/src/utils/bundle-calculator.ts b/packages/angular/build/src/utils/bundle-calculator.ts new file mode 100644 index 000000000000..3349a8a40830 --- /dev/null +++ b/packages/angular/build/src/utils/bundle-calculator.ts @@ -0,0 +1,388 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Budget as BudgetEntry, Type as BudgetType } from '../builders/application/schema'; +import { formatSize } from './format-bytes'; + +// Re-export to avoid direct schema importing throughout code +export { type BudgetEntry, BudgetType }; + +export const BYTES_IN_KILOBYTE = 1000; + +interface Size { + size: number; + label?: string; +} + +export interface Threshold { + limit: number; + type: ThresholdType; + severity: ThresholdSeverity; +} + +enum ThresholdType { + Max = 'maximum', + Min = 'minimum', +} + +export enum ThresholdSeverity { + Warning = 'warning', + Error = 'error', +} + +export interface BudgetCalculatorResult { + severity: ThresholdSeverity; + message: string; + label?: string; +} + +export interface BudgetChunk { + files?: string[]; + names?: string[]; + initial?: boolean; +} + +export interface BudgetAsset { + name: string; + size: number; + componentStyle?: boolean; +} + +export interface BudgetStats { + chunks?: BudgetChunk[]; + assets?: BudgetAsset[]; +} + +export function* calculateThresholds(budget: BudgetEntry): IterableIterator { + if (budget.maximumWarning) { + yield { + limit: calculateBytes(budget.maximumWarning, budget.baseline, 1), + type: ThresholdType.Max, + severity: ThresholdSeverity.Warning, + }; + } + + if (budget.maximumError) { + yield { + limit: calculateBytes(budget.maximumError, budget.baseline, 1), + type: ThresholdType.Max, + severity: ThresholdSeverity.Error, + }; + } + + if (budget.minimumWarning) { + yield { + limit: calculateBytes(budget.minimumWarning, budget.baseline, -1), + type: ThresholdType.Min, + severity: ThresholdSeverity.Warning, + }; + } + + if (budget.minimumError) { + yield { + limit: calculateBytes(budget.minimumError, budget.baseline, -1), + type: ThresholdType.Min, + severity: ThresholdSeverity.Error, + }; + } + + if (budget.warning) { + yield { + limit: calculateBytes(budget.warning, budget.baseline, -1), + type: ThresholdType.Min, + severity: ThresholdSeverity.Warning, + }; + + yield { + limit: calculateBytes(budget.warning, budget.baseline, 1), + type: ThresholdType.Max, + severity: ThresholdSeverity.Warning, + }; + } + + if (budget.error) { + yield { + limit: calculateBytes(budget.error, budget.baseline, -1), + type: ThresholdType.Min, + severity: ThresholdSeverity.Error, + }; + + yield { + limit: calculateBytes(budget.error, budget.baseline, 1), + type: ThresholdType.Max, + severity: ThresholdSeverity.Error, + }; + } +} + +/** + * Calculates the sizes for bundles in the budget type provided. + */ +function calculateSizes(budget: BudgetEntry, stats: BudgetStats): Size[] { + type CalculatorTypes = { + new (budget: BudgetEntry, chunks: BudgetChunk[], assets: BudgetAsset[]): Calculator; + }; + const calculatorMap: Record = { + all: AllCalculator, + allScript: AllScriptCalculator, + any: AnyCalculator, + anyScript: AnyScriptCalculator, + anyComponentStyle: AnyComponentStyleCalculator, + bundle: BundleCalculator, + initial: InitialCalculator, + }; + + const ctor = calculatorMap[budget.type]; + const { chunks, assets } = stats; + if (!chunks) { + throw new Error('Webpack stats output did not include chunk information.'); + } + if (!assets) { + throw new Error('Webpack stats output did not include asset information.'); + } + + const calculator = new ctor(budget, chunks, assets); + + return calculator.calculate(); +} + +abstract class Calculator { + constructor( + protected budget: BudgetEntry, + protected chunks: BudgetChunk[], + protected assets: BudgetAsset[], + ) {} + + abstract calculate(): Size[]; + + /** Calculates the size of the given chunk for the provided build type. */ + protected calculateChunkSize(chunk: BudgetChunk): number { + // No differential builds, get the chunk size by summing its assets. + if (!chunk.files) { + return 0; + } + + return chunk.files + .filter((file) => !file.endsWith('.map')) + .map((file) => { + const asset = this.assets.find((asset) => asset.name === file); + if (!asset) { + throw new Error(`Could not find asset for file: ${file}`); + } + + return asset.size; + }) + .reduce((l, r) => l + r, 0); + } + + protected getAssetSize(asset: BudgetAsset): number { + return asset.size; + } +} + +/** + * A named bundle. + */ +class BundleCalculator extends Calculator { + calculate() { + const budgetName = this.budget.name; + if (!budgetName) { + return []; + } + + const size = this.chunks + .filter((chunk) => chunk?.names?.includes(budgetName)) + .map((chunk) => this.calculateChunkSize(chunk)) + .reduce((l, r) => l + r, 0); + + return [{ size, label: this.budget.name }]; + } +} + +/** + * The sum of all initial chunks (marked as initial). + */ +class InitialCalculator extends Calculator { + calculate() { + return [ + { + label: `bundle initial`, + size: this.chunks + .filter((chunk) => chunk.initial) + .map((chunk) => this.calculateChunkSize(chunk)) + .reduce((l, r) => l + r, 0), + }, + ]; + } +} + +/** + * The sum of all the scripts portions. + */ +class AllScriptCalculator extends Calculator { + calculate() { + const size = this.assets + .filter((asset) => asset.name.endsWith('.js')) + .map((asset) => this.getAssetSize(asset)) + .reduce((total: number, size: number) => total + size, 0); + + return [{ size, label: 'total scripts' }]; + } +} + +/** + * All scripts and assets added together. + */ +class AllCalculator extends Calculator { + calculate() { + const size = this.assets + .filter((asset) => !asset.name.endsWith('.map') && !asset.componentStyle) + .map((asset) => this.getAssetSize(asset)) + .reduce((total: number, size: number) => total + size, 0); + + return [{ size, label: 'total' }]; + } +} + +/** + * Any script, individually. + */ +class AnyScriptCalculator extends Calculator { + calculate() { + return this.assets + .filter((asset) => asset.name.endsWith('.js')) + .map((asset) => ({ + size: this.getAssetSize(asset), + label: asset.name, + })); + } +} + +/** + * Any script or asset (images, css, etc). + */ +class AnyCalculator extends Calculator { + calculate() { + return this.assets + .filter((asset) => !asset.name.endsWith('.map') && !asset.componentStyle) + .map((asset) => ({ + size: this.getAssetSize(asset), + label: asset.name, + })); + } +} + +/** + * Any compoonent stylesheet + */ +class AnyComponentStyleCalculator extends Calculator { + calculate() { + return this.assets + .filter((asset) => asset.componentStyle) + .map((asset) => ({ + size: this.getAssetSize(asset), + label: asset.name, + })); + } +} + +/** + * Calculate the bytes given a string value. + */ +function calculateBytes(input: string, baseline?: string, factor: 1 | -1 = 1): number { + const matches = input.trim().match(/^(\d+(?:\.\d+)?)[ \t]*(%|[kmg]?b)?$/i); + if (!matches) { + return NaN; + } + + const baselineBytes = (baseline && calculateBytes(baseline)) || 0; + + let value = Number(matches[1]); + switch (matches[2] && matches[2].toLowerCase()) { + case '%': + value = (baselineBytes * value) / 100; + break; + case 'kb': + value *= BYTES_IN_KILOBYTE; + break; + case 'mb': + value *= BYTES_IN_KILOBYTE * BYTES_IN_KILOBYTE; + break; + case 'gb': + value *= BYTES_IN_KILOBYTE * BYTES_IN_KILOBYTE * BYTES_IN_KILOBYTE; + break; + } + + if (baselineBytes === 0) { + return value; + } + + return baselineBytes + value * factor; +} + +export function* checkBudgets( + budgets: BudgetEntry[], + stats: BudgetStats, + checkComponentStyles?: boolean, +): IterableIterator { + // Ignore AnyComponentStyle budgets as these are handled in `AnyComponentStyleBudgetChecker` unless requested + const computableBudgets = checkComponentStyles + ? budgets + : budgets.filter((budget) => budget.type !== BudgetType.AnyComponentStyle); + + for (const budget of computableBudgets) { + const sizes = calculateSizes(budget, stats); + for (const { size, label } of sizes) { + yield* checkThresholds(calculateThresholds(budget), size, label); + } + } +} + +export function* checkThresholds( + thresholds: Iterable, + size: number, + label?: string, +): IterableIterator { + for (const threshold of thresholds) { + switch (threshold.type) { + case ThresholdType.Max: { + if (size <= threshold.limit) { + continue; + } + + const sizeDifference = formatSize(size - threshold.limit); + yield { + severity: threshold.severity, + label, + message: `${label} exceeded maximum budget. Budget ${formatSize( + threshold.limit, + )} was not met by ${sizeDifference} with a total of ${formatSize(size)}.`, + }; + break; + } + case ThresholdType.Min: { + if (size >= threshold.limit) { + continue; + } + + const sizeDifference = formatSize(threshold.limit - size); + yield { + severity: threshold.severity, + label, + message: `${label} failed to meet minimum budget. Budget ${formatSize( + threshold.limit, + )} was not met by ${sizeDifference} with a total of ${formatSize(size)}.`, + }; + break; + } + default: { + throw new Error(`Unexpected threshold type: ${ThresholdType[threshold.type]}`); + } + } + } +} diff --git a/packages/angular/build/src/utils/bundle-calculator_spec.ts b/packages/angular/build/src/utils/bundle-calculator_spec.ts new file mode 100644 index 000000000000..9bb44394496b --- /dev/null +++ b/packages/angular/build/src/utils/bundle-calculator_spec.ts @@ -0,0 +1,379 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { + BYTES_IN_KILOBYTE, + BudgetEntry, + BudgetType, + ThresholdSeverity, + checkBudgets, +} from './bundle-calculator'; + +describe('bundle-calculator', () => { + describe('checkBudgets()', () => { + it('yields maximum budgets exceeded', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Any, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [], + assets: [ + { + name: 'foo.js', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'foo.js', + message: jasmine.stringMatching('foo.js exceeded maximum budget.'), + }); + }); + + it('yields minimum budgets exceeded', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Any, + minimumError: '1kb', + }, + ]; + const stats = { + chunks: [], + assets: [ + { + name: 'foo.js', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'bar.js', + message: jasmine.stringMatching('bar.js failed to meet minimum budget.'), + }); + }); + + it('yields exceeded bundle budgets', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Bundle, + name: 'foo', + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + names: ['foo'], + files: ['foo.js', 'bar.js'], + }, + ], + assets: [ + { + name: 'foo.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'foo', + message: jasmine.stringMatching('foo exceeded maximum budget.'), + }); + }); + + it('yields exceeded initial budget', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Initial, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.js', 'bar.js'], + }, + ], + assets: [ + { + name: 'foo.js', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'bundle initial', + message: jasmine.stringMatching('initial exceeded maximum budget.'), + }); + }); + + it('yields exceeded total scripts budget', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.AllScript, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.js', 'bar.js'], + }, + ], + assets: [ + { + name: 'foo.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + { + name: 'baz.css', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'total scripts', + message: jasmine.stringMatching('total scripts exceeded maximum budget.'), + }); + }); + + it('yields exceeded total budget', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.All, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.js', 'bar.css'], + }, + ], + assets: [ + { + name: 'foo.js', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.css', + size: 0.75 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'total', + message: jasmine.stringMatching('total exceeded maximum budget.'), + }); + }); + + it('skips component style budgets', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.AnyComponentStyle, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.css', 'bar.js'], + }, + ], + assets: [ + { + name: 'foo.css', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(0); + }); + + it('yields exceeded individual script budget', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.AnyScript, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.js', 'bar.js'], + }, + ], + assets: [ + { + name: 'foo.js', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.js', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'foo.js', + message: jasmine.stringMatching('foo.js exceeded maximum budget.'), + }); + }); + + it('yields exceeded individual file budget', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Any, + maximumError: '1kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['foo'], + files: ['foo.ext', 'bar.ext'], + }, + ], + assets: [ + { + name: 'foo.ext', + size: 1.5 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.ext', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures.length).toBe(1); + expect(failures).toContain({ + severity: ThresholdSeverity.Error, + label: 'foo.ext', + message: jasmine.stringMatching('foo.ext exceeded maximum budget.'), + }); + }); + + it('does not exceed the individual file budget limit', () => { + const budgets: BudgetEntry[] = [ + { + type: BudgetType.Bundle, + maximumError: '1000kb', + }, + ]; + const stats = { + chunks: [ + { + id: 0, + initial: true, + names: ['main'], + files: ['main.ext', 'bar.ext'], + }, + ], + assets: [ + { + name: 'main.ext', + size: 1 * BYTES_IN_KILOBYTE, + }, + { + name: 'bar.ext', + size: 0.5 * BYTES_IN_KILOBYTE, + }, + ], + }; + + const failures = Array.from(checkBudgets(budgets, stats)); + + expect(failures).toHaveSize(0); + }); + }); +}); diff --git a/packages/angular/build/src/utils/check-port.ts b/packages/angular/build/src/utils/check-port.ts new file mode 100644 index 000000000000..d7c04f0b9f72 --- /dev/null +++ b/packages/angular/build/src/utils/check-port.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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:net'; +import { isTTY } from './tty'; + +function createInUseError(port: number): Error { + return new Error(`Port ${port} is already in use. Use '--port' to specify a different port.`); +} + +export async function checkPort(port: number, host: string): Promise { + // Disabled due to Vite not handling port 0 and instead always using the default value (5173) + // TODO: Enable this again once Vite is fixed + // if (port === 0) { + // return 0; + // } + + return new Promise((resolve, reject) => { + const server = createServer(); + + server + .once('error', (err: NodeJS.ErrnoException) => { + if (err.code !== 'EADDRINUSE') { + reject(err); + + return; + } + + if (!isTTY()) { + reject(createInUseError(port)); + + return; + } + + import('@inquirer/confirm') + .then(({ default: confirm }) => + confirm({ + message: `Port ${port} is already in use.\nWould you like to use a different port?`, + default: true, + theme: { prefix: '' }, + }), + ) + .then( + (answer) => (answer ? resolve(checkPort(0, host)) : reject(createInUseError(port))), + () => reject(createInUseError(port)), + ); + }) + .once('listening', () => { + // Get the actual address from the listening server instance + const address = server.address(); + assert( + address && typeof address !== 'string', + 'Port check server address should always be an object.', + ); + + server.close(); + resolve(address.port); + }) + .listen(port, host); + }); +} diff --git a/packages/angular/build/src/utils/color.ts b/packages/angular/build/src/utils/color.ts new file mode 100644 index 000000000000..3915d99ce248 --- /dev/null +++ b/packages/angular/build/src/utils/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/build/src/utils/delete-output-dir.ts b/packages/angular/build/src/utils/delete-output-dir.ts new file mode 100644 index 000000000000..45084760793d --- /dev/null +++ b/packages/angular/build/src/utils/delete-output-dir.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 { readdir, rm } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; + +/** + * Delete an output directory, but error out if it's the root of the project. + */ +export async function deleteOutputDir( + root: string, + outputPath: string, + emptyOnlyDirectories?: string[], +): Promise { + const resolvedOutputPath = resolve(root, outputPath); + if (resolvedOutputPath === root) { + throw new Error('Output path MUST not be project root directory!'); + } + + const directoriesToEmpty = emptyOnlyDirectories + ? new Set(emptyOnlyDirectories.map((directory) => join(resolvedOutputPath, directory))) + : undefined; + + // Avoid removing the actual directory to avoid errors in cases where the output + // directory is mounted or symlinked. Instead the contents are removed. + let entries; + try { + entries = await readdir(resolvedOutputPath); + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + return; + } + throw error; + } + + for (const entry of entries) { + const fullEntry = join(resolvedOutputPath, entry); + + // Leave requested directories. This allows symlinks to continue to function. + if (directoriesToEmpty?.has(fullEntry)) { + await deleteOutputDir(resolvedOutputPath, fullEntry); + continue; + } + + await rm(fullEntry, { force: true, recursive: true, maxRetries: 3 }); + } +} diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts new file mode 100644 index 000000000000..80f71d56c119 --- /dev/null +++ b/packages/angular/build/src/utils/environment-options.ts @@ -0,0 +1,175 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { availableParallelism } from 'node:os'; + +/** A set of strings that are considered "truthy" when parsing environment variables. */ +const TRUTHY_VALUES = new Set(['1', 'true']); + +/** A set of strings that are considered "falsy" when parsing environment variables. */ +const FALSY_VALUES = new Set(['0', 'false']); + +/** + * Checks if an environment variable is present and has a non-empty value. + * @param variable The environment variable to check. + * @returns `true` if the variable is a non-empty string. + */ +function isPresent(variable: string | undefined): variable is string { + return typeof variable === 'string' && variable !== ''; +} + +/** + * Parses an environment variable into a boolean or undefined. + * @returns `true` if the variable is truthy ('1', 'true'). + * @returns `false` if the variable is falsy ('0', 'false'). + * @returns `undefined` if the variable is not present or has an unknown value. + */ +function parseTristate(variable: string | undefined): boolean | undefined { + if (!isPresent(variable)) { + return undefined; + } + + const value = variable.toLowerCase(); + if (TRUTHY_VALUES.has(value)) { + return true; + } + if (FALSY_VALUES.has(value)) { + return false; + } + + // TODO: Consider whether a warning is useful in this case of a malformed value + return undefined; +} + +// Optimization and mangling +const debugOptimizeVariable = process.env['NG_BUILD_DEBUG_OPTIMIZE']; +const debugOptimize = (() => { + if (!isPresent(debugOptimizeVariable) || parseTristate(debugOptimizeVariable) === false) { + return { + mangle: true, + minify: true, + beautify: false, + }; + } + + const debugValue = { + mangle: false, + minify: false, + beautify: true, + }; + + if (parseTristate(debugOptimizeVariable) === true) { + return debugValue; + } + + for (const part of debugOptimizeVariable.split(',')) { + switch (part.trim().toLowerCase()) { + case 'mangle': + debugValue.mangle = true; + break; + case 'minify': + debugValue.minify = true; + break; + case 'beautify': + debugValue.beautify = true; + break; + } + } + + return debugValue; +})(); + +/** + * Allows disabling of code mangling when the `NG_BUILD_MANGLE` environment variable is set to `0` or `false`. + * This is useful for debugging build output. + */ +export const allowMangle = parseTristate(process.env['NG_BUILD_MANGLE']) ?? debugOptimize.mangle; + +/** + * Allows beautification of build output when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. + * This is useful for debugging build output. + */ +export const shouldBeautify = debugOptimize.beautify; + +/** + * Allows disabling of code minification when the `NG_BUILD_DEBUG_OPTIMIZE` environment variable is enabled. + * This is useful for debugging build output. + */ +export const allowMinify = debugOptimize.minify; + +/** + * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. + * This cause `Error: Call retries were exceeded` errors when trying to use them. + * + * @see https://github.com/nodejs/node/issues/28762 + * @see https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 + * @see https://ithub.com/angular/angular-cli/issues/16860#issuecomment-588828079 + * + */ +const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; + +/** + * The maximum number of workers to use for parallel processing. + * This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable. + */ +export const maxWorkers = isPresent(maxWorkersVariable) + ? +maxWorkersVariable + : Math.min(4, Math.max(availableParallelism() - 1, 1)); + +/** + * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. + */ +export const useParallelTs = parseTristate(process.env['NG_BUILD_PARALLEL_TS']) !== false; + +/** + * When `NG_BUILD_DEBUG_PERF` is enabled, performance debugging information is printed. + */ +export const debugPerformance = parseTristate(process.env['NG_BUILD_DEBUG_PERF']) === true; + +/** + * When `NG_BUILD_WATCH_ROOT` is enabled, the build will watch the root directory for changes. + */ +export const shouldWatchRoot = parseTristate(process.env['NG_BUILD_WATCH_ROOT']) === true; + +/** + * When `NG_BUILD_TYPE_CHECK` is set to `0` or `false`, type checking is disabled. + */ +export const useTypeChecking = parseTristate(process.env['NG_BUILD_TYPE_CHECK']) !== false; + +/** + * When `NG_BUILD_LOGS_JSON` is enabled, build logs will be output in JSON format. + */ +export const useJSONBuildLogs = parseTristate(process.env['NG_BUILD_LOGS_JSON']) === true; + +/** + * When `NG_BUILD_OPTIMIZE_CHUNKS` is enabled, the build will optimize chunks. + */ +export const shouldOptimizeChunks = parseTristate(process.env['NG_BUILD_OPTIMIZE_CHUNKS']) === true; + +/** + * When `NG_HMR_CSTYLES` is enabled, component styles will be hot-reloaded. + */ +export const useComponentStyleHmr = parseTristate(process.env['NG_HMR_CSTYLES']) === true; + +/** + * When `NG_HMR_TEMPLATES` is set to `0` or `false`, component templates will not be hot-reloaded. + */ +export const useComponentTemplateHmr = parseTristate(process.env['NG_HMR_TEMPLATES']) !== false; + +/** + * When `NG_BUILD_PARTIAL_SSR` is enabled, a partial server-side rendering build will be performed. + */ +export const usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SSR']) === true; + +const bazelBinDirectory = process.env['BAZEL_BINDIR']; +const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; + +export const bazelEsbuildPluginPath = + bazelBinDirectory && bazelExecRoot + ? process.env['NG_INTERNAL_ESBUILD_PLUGINS_DO_NOT_USE'] + : undefined; diff --git a/packages/angular/build/src/utils/error.ts b/packages/angular/build/src/utils/error.ts new file mode 100644 index 000000000000..0ca77c331d2d --- /dev/null +++ b/packages/angular/build/src/utils/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/build/src/utils/format-bytes.ts b/packages/angular/build/src/utils/format-bytes.ts new file mode 100644 index 000000000000..5c9ecee6875a --- /dev/null +++ b/packages/angular/build/src/utils/format-bytes.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 + */ + +export function 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(1000)); + const roundedSize = size / Math.pow(1000, index); + // bytes don't have a fraction + const fractionDigits = index === 0 ? 0 : 2; + + return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`; +} diff --git a/packages/angular/build/src/utils/format-bytes_spec.ts b/packages/angular/build/src/utils/format-bytes_spec.ts new file mode 100644 index 000000000000..63cb3f761ecf --- /dev/null +++ b/packages/angular/build/src/utils/format-bytes_spec.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { formatSize } from './format-bytes'; + +describe('formatSize', () => { + it('1000 bytes to be 1kB', () => { + expect(formatSize(1000)).toBe('1.00 kB'); + }); + + it('1_000_000 bytes to be 1MB', () => { + expect(formatSize(1_000_000)).toBe('1.00 MB'); + }); + + it('1_500_000 bytes to be 1.5MB', () => { + expect(formatSize(1_500_000)).toBe('1.50 MB'); + }); + + it('1_000_000_000 bytes to be 1GB', () => { + expect(formatSize(1_000_000_000)).toBe('1.00 GB'); + }); +}); diff --git a/packages/angular/build/src/utils/i18n-options.ts b/packages/angular/build/src/utils/i18n-options.ts new file mode 100644 index 000000000000..822683bef03d --- /dev/null +++ b/packages/angular/build/src/utils/i18n-options.ts @@ -0,0 +1,293 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 type { TranslationLoader } from './load-translations'; + +export interface LocaleDescription { + files: { + path: string; + integrity?: string; + format?: string; + }[]; + translation?: Record; + dataPath?: string; + baseHref?: string; + subPath: string; +} + +export interface I18nOptions { + inlineLocales: Set; + sourceLocale: string; + locales: Record; + flatOutput?: boolean; + readonly shouldInline: boolean; + hasDefinedSourceLocale?: boolean; +} + +function normalizeTranslationFileOption( + option: unknown, + locale: string, + expectObjectInError: boolean, +): string[] { + if (typeof option === 'string') { + return [option]; + } + + if (Array.isArray(option) && option.every((element) => typeof element === 'string')) { + return option; + } + + let errorMessage = `Project i18n locales translation field value for '${locale}' is malformed. `; + if (expectObjectInError) { + errorMessage += 'Expected a string, array of strings, or object.'; + } else { + errorMessage += 'Expected a string or array of strings.'; + } + + throw new Error(errorMessage); +} + +function ensureObject(value: unknown, name: string): asserts value is Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Project field '${name}' is malformed. Expected an object.`); + } +} + +function ensureString(value: unknown, name: string): asserts value is string { + if (typeof value !== 'string') { + throw new Error(`Project field '${name}' is malformed. Expected a string.`); + } +} + +function ensureValidSubPath(value: unknown, name: string): asserts value is string { + ensureString(value, name); + + if (!/^[\w-]*$/.test(value)) { + throw new Error( + `Project field '${name}' is invalid. It can only contain letters, numbers, hyphens, and underscores.`, + ); + } +} +export function createI18nOptions( + projectMetadata: { i18n?: unknown }, + inline?: boolean | string[], + logger?: { + warn(message: string): void; + }, + ssrEnabled?: boolean, +): I18nOptions { + const { i18n: metadata = {} } = projectMetadata; + + ensureObject(metadata, 'i18n'); + + const i18n: I18nOptions = { + inlineLocales: new Set(), + // en-US is the default locale added to Angular applications (https://angular.dev/guide/i18n/format-data-locale) + sourceLocale: 'en-US', + locales: {}, + get shouldInline() { + return this.inlineLocales.size > 0; + }, + }; + + let rawSourceLocale: string | undefined; + let rawSourceLocaleBaseHref: string | undefined; + let rawsubPath: string | undefined; + if (typeof metadata.sourceLocale === 'string') { + rawSourceLocale = metadata.sourceLocale; + } else if (metadata.sourceLocale !== undefined) { + ensureObject(metadata.sourceLocale, 'i18n.sourceLocale'); + + if (metadata.sourceLocale.code !== undefined) { + ensureString(metadata.sourceLocale.code, 'i18n.sourceLocale.code'); + rawSourceLocale = metadata.sourceLocale.code; + } + + if (metadata.sourceLocale.baseHref !== undefined) { + ensureString(metadata.sourceLocale.baseHref, 'i18n.sourceLocale.baseHref'); + if (ssrEnabled) { + logger?.warn( + `'baseHref' in 'i18n.sourceLocale' may lead to undefined behavior when used with SSR. ` + + `Consider using 'subPath' instead.\n\n` + + `Note: 'subPath' specifies the URL segment for the locale, serving as both the HTML base HREF ` + + `and the output directory name.\nBy default, if not explicitly set, 'subPath' defaults to the locale code.`, + ); + } + + rawSourceLocaleBaseHref = metadata.sourceLocale.baseHref; + } + + if (metadata.sourceLocale.subPath !== undefined) { + ensureValidSubPath(metadata.sourceLocale.subPath, 'i18n.sourceLocale.subPath'); + rawsubPath = metadata.sourceLocale.subPath; + } + + if (rawsubPath !== undefined && rawSourceLocaleBaseHref !== undefined) { + throw new Error( + `'i18n.sourceLocale.subPath' and 'i18n.sourceLocale.baseHref' cannot be used together.`, + ); + } + } + + if (rawSourceLocale !== undefined) { + i18n.sourceLocale = rawSourceLocale; + i18n.hasDefinedSourceLocale = true; + } + + i18n.locales[i18n.sourceLocale] = { + files: [], + baseHref: rawSourceLocaleBaseHref, + subPath: rawsubPath ?? i18n.sourceLocale, + }; + + if (metadata.locales !== undefined) { + ensureObject(metadata.locales, 'i18n locales'); + + for (const [locale, options] of Object.entries(metadata.locales)) { + let translationFiles: string[] | undefined; + let baseHref: string | undefined; + let subPath: string | undefined; + + if (options && typeof options === 'object' && 'translation' in options) { + translationFiles = normalizeTranslationFileOption(options.translation, locale, false); + + if ('baseHref' in options) { + ensureString(options.baseHref, `i18n.locales.${locale}.baseHref`); + + if (ssrEnabled) { + logger?.warn( + `'baseHref' in 'i18n.locales.${locale}' may lead to undefined behavior when used with SSR. ` + + `Consider using 'subPath' instead.\n\n` + + `Note: 'subPath' specifies the URL segment for the locale, serving as both the HTML base HREF ` + + `and the output directory name.\nBy default, if not explicitly set, 'subPath' defaults to the locale code.`, + ); + } + baseHref = options.baseHref; + } + + if ('subPath' in options) { + ensureValidSubPath(options.subPath, `i18n.locales.${locale}.subPath`); + subPath = options.subPath; + } + + if (subPath !== undefined && baseHref !== undefined) { + throw new Error( + `'i18n.locales.${locale}.subPath' and 'i18n.locales.${locale}.baseHref' cannot be used together.`, + ); + } + } else { + translationFiles = normalizeTranslationFileOption(options, locale, true); + } + + if (locale === i18n.sourceLocale) { + throw new Error( + `An i18n locale ('${locale}') cannot both be a source locale and provide a translation.`, + ); + } + + i18n.locales[locale] = { + files: translationFiles.map((file) => ({ path: file })), + baseHref, + subPath: subPath ?? locale, + }; + } + } + + if (inline === true) { + i18n.inlineLocales.add(i18n.sourceLocale); + Object.keys(i18n.locales).forEach((locale) => i18n.inlineLocales.add(locale)); + } else if (inline) { + for (const locale of inline) { + if (!i18n.locales[locale] && i18n.sourceLocale !== locale) { + throw new Error(`Requested locale '${locale}' is not defined for the project.`); + } + + i18n.inlineLocales.add(locale); + } + } + + // Check that subPaths are unique only the locales that we are inlining. + const localesData = Object.entries(i18n.locales).filter(([locale]) => + i18n.inlineLocales.has(locale), + ); + + for (let i = 0; i < localesData.length; i++) { + const [localeA, { subPath: subPathA }] = localesData[i]; + + for (let j = i + 1; j < localesData.length; j++) { + const [localeB, { subPath: subPathB }] = localesData[j]; + + if (subPathA === subPathB) { + throw new Error( + `Invalid i18n configuration: Locales '${localeA}' and '${localeB}' cannot have the same subPath: '${subPathB}'.`, + ); + } + } + } + + return i18n; +} + +export function loadTranslations( + locale: string, + desc: LocaleDescription, + workspaceRoot: string, + loader: TranslationLoader, + logger: { warn: (message: string) => void; error: (message: string) => void }, + usedFormats?: Set, + duplicateTranslation?: 'ignore' | 'error' | 'warning', +) { + let translations: Record | undefined = undefined; + for (const file of desc.files) { + const loadResult = loader(path.join(workspaceRoot, file.path)); + + for (const diagnostics of loadResult.diagnostics.messages) { + if (diagnostics.type === 'error') { + logger.error(`Error parsing translation file '${file.path}': ${diagnostics.message}`); + } else { + logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`); + } + } + + if (loadResult.locale !== undefined && loadResult.locale !== locale) { + logger.warn( + `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`, + ); + } + + usedFormats?.add(loadResult.format); + file.format = loadResult.format; + file.integrity = loadResult.integrity; + + if (translations) { + // Merge translations + for (const [id, message] of Object.entries(loadResult.translations)) { + if (translations[id] !== undefined) { + const duplicateTranslationMessage = `[${file.path}]: Duplicate translations for message '${id}' when merging.`; + switch (duplicateTranslation) { + case 'ignore': + break; + case 'error': + logger.error(`ERROR ${duplicateTranslationMessage}`); + break; + case 'warning': + default: + logger.warn(`WARNING ${duplicateTranslationMessage}`); + break; + } + } + translations[id] = message; + } + } else { + // First or only translation file + translations = loadResult.translations; + } + } + desc.translation = translations; +} diff --git a/packages/angular/build/src/utils/index-file/add-event-dispatch-contract.ts b/packages/angular/build/src/utils/index-file/add-event-dispatch-contract.ts new file mode 100644 index 000000000000..749a489f8a4a --- /dev/null +++ b/packages/angular/build/src/utils/index-file/add-event-dispatch-contract.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 { readFile } from 'node:fs/promises'; +import { htmlRewritingStream } from './html-rewriting-stream'; + +let jsActionContractScript: string; + +export async function addEventDispatchContract(html: string): Promise { + const { rewriter, transformedContent } = await htmlRewritingStream(html); + + jsActionContractScript ??= + ''; + + rewriter.on('startTag', (tag) => { + rewriter.emitStartTag(tag); + + if (tag.tagName === 'body') { + rewriter.emitRaw(jsActionContractScript); + } + }); + + return transformedContent(); +} diff --git a/packages/angular/build/src/utils/index-file/add-event-dispatch-contract_spec.ts b/packages/angular/build/src/utils/index-file/add-event-dispatch-contract_spec.ts new file mode 100644 index 000000000000..6c0747730c29 --- /dev/null +++ b/packages/angular/build/src/utils/index-file/add-event-dispatch-contract_spec.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 { addEventDispatchContract } from './add-event-dispatch-contract'; + +describe('addEventDispatchContract', () => { + it('should inline event dispatcher script', async () => { + const result = await addEventDispatchContract(` + + + +

Hello World!

+ + + `); + + expect(result).toMatch( + /\s*`); + } + + let headerLinkTags: string[] = []; + let bodyLinkTags: string[] = []; + for (const src of stylesheets) { + const attrs = [`rel="stylesheet"`, `href="${generateUrl(src, deployUrl)}"`]; + + if (crossOrigin !== 'none') { + attrs.push(`crossorigin="${crossOrigin}"`); + } + + if (sri) { + const content = await loadOutputFile(src); + attrs.push(generateSriAttributes(content)); + } + + headerLinkTags.push(``); + } + + if (params.hints?.length) { + for (const hint of params.hints) { + const attrs = [`rel="${hint.mode}"`, `href="${generateUrl(hint.url, deployUrl)}"`]; + + if (hint.mode !== 'modulepreload' && crossOrigin !== 'none') { + // Value is considered anonymous by the browser when not present or empty + attrs.push(crossOrigin === 'anonymous' ? 'crossorigin' : `crossorigin="${crossOrigin}"`); + } + + if (hint.mode === 'preload' || hint.mode === 'prefetch') { + switch (extname(hint.url)) { + case '.js': + attrs.push('as="script"'); + break; + case '.css': + attrs.push('as="style"'); + break; + default: + if (hint.as) { + attrs.push(`as="${hint.as}"`); + } + break; + } + } + + if ( + sri && + (hint.mode === 'preload' || hint.mode === 'prefetch' || hint.mode === 'modulepreload') + ) { + const content = await loadOutputFile(hint.url); + attrs.push(generateSriAttributes(content)); + } + + const tag = ``; + if (hint.mode === 'modulepreload') { + // Module preloads should be placed by the inserted script elements in the body since + // they are only useful in combination with the scripts. + bodyLinkTags.push(tag); + } else { + headerLinkTags.push(tag); + } + } + } + + const dir = lang ? await getLanguageDirection(lang, warnings) : undefined; + const { rewriter, transformedContent } = await htmlRewritingStream(html); + const baseTagExists = html.includes('(); + + rewriter + .on('startTag', (tag, rawTagHtml) => { + switch (tag.tagName) { + case 'html': + // Adjust document locale if specified + if (isString(lang)) { + updateAttribute(tag, 'lang', lang); + } + + if (dir) { + updateAttribute(tag, 'dir', dir); + } + break; + case 'head': + // Base href should be added before any link, meta tags + if (!baseTagExists && isString(baseHref)) { + rewriter.emitStartTag(tag); + rewriter.emitRaw(``); + + return; + } + break; + case 'base': + // Adjust base href if specified + if (isString(baseHref)) { + updateAttribute(tag, 'href', baseHref); + } + break; + case 'link': + if (readAttribute(tag, 'rel') === 'preconnect') { + const href = readAttribute(tag, 'href'); + if (href) { + foundPreconnects.add(href); + } + } + break; + default: + if (tag.selfClosing && !VALID_SELF_CLOSING_TAGS.has(tag.tagName)) { + errors.push(`Invalid self-closing element in index HTML file: '${rawTagHtml}'.`); + + return; + } + } + + rewriter.emitStartTag(tag); + }) + .on('endTag', (tag) => { + switch (tag.tagName) { + case 'head': + for (const linkTag of headerLinkTags) { + rewriter.emitRaw(linkTag); + } + if (imageDomains) { + for (const imageDomain of imageDomains) { + if (!foundPreconnects.has(imageDomain)) { + rewriter.emitRaw(``); + } + } + } + headerLinkTags = []; + break; + case 'body': + for (const linkTag of bodyLinkTags) { + rewriter.emitRaw(linkTag); + } + bodyLinkTags = []; + + // Add script tags + for (const scriptTag of scriptTags) { + rewriter.emitRaw(scriptTag); + } + + scriptTags = []; + break; + } + + rewriter.emitEndTag(tag); + }); + + const content = await transformedContent(); + + return { + content: + headerLinkTags.length || scriptTags.length + ? // In case no body/head tags are not present (dotnet partial templates) + headerLinkTags.join('') + scriptTags.join('') + content + : content, + warnings, + errors, + }; +} + +function generateSriAttributes(content: string): string { + const algo = 'sha384'; + const hash = createHash(algo).update(content, 'utf8').digest('base64'); + + return `integrity="${algo}-${hash}"`; +} + +function generateUrl(value: string, deployUrl: string | undefined): string { + if (!deployUrl) { + return value; + } + + // Skip if root-relative, absolute or protocol relative url + if (/^((?:\w+:)?\/\/|data:|chrome:|\/)/.test(value)) { + return value; + } + + return `${deployUrl}${value}`; +} + +function updateAttribute( + tag: { attrs: { name: string; value: string }[] }, + name: string, + value: string, +): void { + const index = tag.attrs.findIndex((a) => a.name === name); + const newValue = { name, value }; + + if (index === -1) { + tag.attrs.push(newValue); + } else { + tag.attrs[index] = newValue; + } +} + +function readAttribute( + tag: { attrs: { name: string; value: string }[] }, + name: string, +): string | undefined { + const targetAttr = tag.attrs.find((attr) => attr.name === name); + + return targetAttr ? targetAttr.value : undefined; +} + +function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +async function getLanguageDirection( + locale: string, + warnings: string[], +): Promise { + const dir = await getLanguageDirectionFromLocales(locale); + + if (!dir) { + warnings.push( + `Locale data for '${locale}' cannot be found. 'dir' attribute will not be set for this locale.`, + ); + } + + return dir; +} + +async function getLanguageDirectionFromLocales(locale: string): Promise { + try { + const localeData = (await import(`@angular/common/locales/${locale}`)).default; + const dir = localeData[localeData.length - 2]; + + return isString(dir) ? dir : undefined; + } catch { + // In some cases certain locales might map to files which are named only with language id. + // Example: `en-US` -> `en`. + const [languageId] = locale.split('-', 1); + if (languageId !== locale) { + return getLanguageDirectionFromLocales(languageId); + } + } + + return undefined; +} diff --git a/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts new file mode 100644 index 000000000000..55adf8d88f0b --- /dev/null +++ b/packages/angular/build/src/utils/index-file/augment-index-html_spec.ts @@ -0,0 +1,576 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { AugmentIndexHtmlOptions, augmentIndexHtml } from './augment-index-html'; + +describe('augment-index-html', () => { + const indexGeneratorOptions: AugmentIndexHtmlOptions = { + html: '', + baseHref: '/', + sri: false, + files: [], + loadOutputFile: async (_fileName: string) => '', + entrypoints: [ + ['scripts', false], + ['polyfills', true], + ['main', true], + ['styles', false], + ], + }; + + const oneLineHtml = (html: TemplateStringsArray) => + `${html}`.replace(/(>\s+)/g, '>').replace(/\s+ { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + files: [ + { file: 'styles.css', extension: '.css', name: 'styles' }, + { file: 'runtime.js', extension: '.js', name: 'main' }, + { file: 'main.js', extension: '.js', name: 'main' }, + { file: 'runtime.js', extension: '.js', name: 'polyfills' }, + { file: 'polyfills.js', extension: '.js', name: 'polyfills' }, + ], + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + + + + `); + }); + + it('should replace base href value', async () => { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: '', + baseHref: '/Apps/', + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + `); + }); + + it('should add lang and dir LTR attribute for French (fr)', async () => { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + lang: 'fr', + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + `); + }); + + it('should add lang and dir RTL attribute for Pashto (ps)', async () => { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + lang: 'ps', + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + `); + }); + + it(`should fallback to use language ID to set the dir attribute (en-US)`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + lang: 'en-US', + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + `); + }); + + it(`should work when lang (locale) is not provided by '@angular/common'`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + lang: 'xx-XX', + }); + + expect(warnings).toEqual([ + `Locale data for 'xx-XX' cannot be found. 'dir' attribute will not be set for this locale.`, + ]); + expect(content).toEqual(oneLineHtml` + + + + + + + + `); + }); + + it(`should add script and link tags even when body and head element doesn't exist`, async () => { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: ``, + files: [ + { file: 'styles.css', extension: '.css', name: 'styles' }, + { file: 'runtime.js', extension: '.js', name: 'main' }, + { file: 'main.js', extension: '.js', name: 'main' }, + { file: 'runtime.js', extension: '.js', name: 'polyfills' }, + { file: 'polyfills.js', extension: '.js', name: 'polyfills' }, + ], + }); + + expect(content).toEqual(oneLineHtml` + + + + + + `); + }); + + it(`should add preconnect and dns-prefetch hints when provided with cross origin`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'preconnect', url: 'http://example.com' }, + { mode: 'dns-prefetch', url: 'http://example.com' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add preconnect and dns-prefetch hints when provided with "use-credentials" cross origin`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + crossOrigin: 'use-credentials', + hints: [ + { mode: 'preconnect', url: 'http://example.com' }, + { mode: 'dns-prefetch', url: 'http://example.com' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add preconnect and dns-prefetch hints when provided with "anonymous" cross origin`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + crossOrigin: 'anonymous', + hints: [ + { mode: 'preconnect', url: 'http://example.com' }, + { mode: 'dns-prefetch', url: 'http://example.com' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add preconnect and dns-prefetch hints when provided with "none" cross origin`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + crossOrigin: 'none', + hints: [ + { mode: 'preconnect', url: 'http://example.com' }, + { mode: 'dns-prefetch', url: 'http://example.com' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add preconnect and dns-prefetch hints when provided with no cross origin`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'preconnect', url: 'http://example.com' }, + { mode: 'dns-prefetch', url: 'http://example.com' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add modulepreload hint when provided`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'modulepreload', url: 'x.js' }, + { mode: 'modulepreload', url: 'y/z.js' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add modulepreload hint with no crossorigin attribute when provided with cross origin set`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + crossOrigin: 'anonymous', + hints: [ + { mode: 'modulepreload', url: 'x.js' }, + { mode: 'modulepreload', url: 'y/z.js' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add prefetch/preload hints with as=script when specified with a JS url`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'prefetch', url: 'x.js' }, + { mode: 'preload', url: 'y/z.js' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add prefetch/preload hints with as=style when specified with a CSS url`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'prefetch', url: 'x.css' }, + { mode: 'preload', url: 'y/z.css' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should add prefetch/preload hints with as=style when specified with a URL and an 'as' option`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + hints: [ + { mode: 'prefetch', url: 'https://example.com/x?a=1', as: 'style' }, + { mode: 'preload', url: 'http://example.com/y?b=2', as: 'style' }, + ], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it(`should not add deploy URL to hints with an absolute URL`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + deployUrl: 'https://localhost/', + hints: [{ mode: 'preload', url: 'http://example.com/y?b=2' }], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + `); + }); + + it(`should not add deploy URL to hints with a root-relative URL`, async () => { + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + deployUrl: 'https://example.com/', + hints: [{ mode: 'preload', url: '/y?b=2' }], + }); + + expect(warnings).toHaveSize(0); + expect(content).toEqual(oneLineHtml` + + + + + + + + + `); + }); + + it('should add `.mjs` script tags', async () => { + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }], + entrypoints: [['main', true /* isModule */]], + }); + + expect(content).toContain(''); + }); + + it('should reject non-module `.mjs` scripts', async () => { + const options: AugmentIndexHtmlOptions = { + ...indexGeneratorOptions, + files: [{ file: 'main.mjs', extension: '.mjs', name: 'main' }], + entrypoints: [['main', false /* isModule */]], + }; + + await expectAsync(augmentIndexHtml(options)).toBeRejectedWithError( + '`.mjs` files *must* set `isModule` to `true`.', + ); + }); + + it('should add image domain preload tags', async () => { + const imageDomains = ['https://www.example.com', 'https://www.example2.com']; + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + imageDomains, + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it('should add no image preconnects if provided empty domain list', async () => { + const imageDomains: Array = []; + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + imageDomains, + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + `); + }); + + it('should not add duplicate preconnects', async () => { + const imageDomains = ['https://www.example1.com', 'https://www.example2.com']; + const { content, warnings } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: '', + imageDomains, + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + + + `); + }); + + it('should add image preconnects if it encounters preconnect elements for other resources', async () => { + const imageDomains = ['https://www.example2.com', 'https://www.example3.com']; + const { content } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: '', + imageDomains, + }); + + expect(content).toEqual(oneLineHtml` + + + + + + + + + + + `); + }); + + describe('self-closing tags', () => { + it('should return an error when used on a not supported element', async () => { + const { errors } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: ` + + + + + ' + `, + }); + + expect(errors.length).toEqual(1); + expect(errors).toEqual([`Invalid self-closing element in index HTML file: ''.`]); + }); + + it('should not return an error when used on a supported element', async () => { + const { errors } = await augmentIndexHtml({ + ...indexGeneratorOptions, + html: ` + + +
+ + + ' + `, + }); + + expect(errors.length).toEqual(0); + }); + }); +}); diff --git a/packages/angular/build/src/utils/index-file/auto-csp.ts b/packages/angular/build/src/utils/index-file/auto-csp.ts new file mode 100644 index 000000000000..c50e0bfce3f2 --- /dev/null +++ b/packages/angular/build/src/utils/index-file/auto-csp.ts @@ -0,0 +1,303 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 crypto from 'node:crypto'; +import { StartTag, htmlRewritingStream } from './html-rewriting-stream'; + +/** + * The hash function to use for hash directives to use in the CSP. + */ +const HASH_FUNCTION = 'sha256'; + +/** + * Store the appropriate attributes of a sourced script tag to generate the loader script. + */ +interface SrcScriptTag { + src: string; + type?: string; + async: boolean; + defer: boolean; +} + +/** + * Get the specified attribute or return undefined if the tag doesn't have that attribute. + * + * @param tag StartTag of the `); + scriptContent = []; + } + + rewriter.on('startTag', (tag) => { + if (tag.tagName === 'script') { + openedScriptTag = tag; + const src = getScriptAttributeValue(tag, 'src'); + + if (src) { + // If there are any interesting attributes, note them down. + const scriptType = getScriptAttributeValue(tag, 'type'); + if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { + scriptContent.push({ + src: src, + type: scriptType, + async: getScriptAttributeValue(tag, 'async') !== undefined, + defer: getScriptAttributeValue(tag, 'defer') !== undefined, + }); + + return; // Skip writing my script tag until we've read it all. + } + } + } + // We are encountering the first start tag that's not tag if it's a part of the + // dynamic loader script. + if (src && shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) { + return; + } + } + + if (tag.tagName === 'head' || tag.tagName === 'body' || tag.tagName === 'html') { + // Write the loader script if a string of +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + expect(csps[0]).toContain(hashTextContent("console.log('foo');")); + }); + + it('should rewrite a single source script', async () => { + const result = await autoCsp(` + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + expect(result).toContain(`var scripts = [['./main.js', '', false, false]];`); + }); + + it('should rewrite a single source script in place', async () => { + const result = await autoCsp(` + + + + +
Some text
+ + + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + // Our loader script appears after the HTML text content. + expect(result).toMatch( + /Some text<\/div>\s* + + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(TWO_HASH_CSP); + expect(result).toContain( + // eslint-disable-next-line max-len + `var scripts = [['./main1.js', '', false, false],['./main2.js', '', true, false],['./main3.js', 'module', true, true]];`, + ); + // Head loader script is in the head. + expect(result).toContain(``); + // Only two loader scripts are created. + expect(Array.from(result.matchAll(/ + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + // & encodes correctly + expect(result).toContain(`'/foo&bar'`); + // Impossible to escape a string and create invalid loader JS with a ' + // (Quotes and backslashes work) + expect(result).toContain(`'/one\\'two%5C\\'three%5C%5C\\'four%5C%5C%5C\\'five'`); + // HTML entities work + expect(result).toContain(`'/one&two&three&four'`); + // Cannot escape JS context to HTML + expect(result).toContain(`'./%3C/script%3E'`); + }); + + it('should rewrite all script tags', async () => { + const result = await autoCsp(` + + + + + + + + + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + // Exactly four hashes for the four scripts that remain (inline, loader, inline, loader). + expect(csps[0]).toMatch(FOUR_HASH_CSP); + expect(csps[0]).toContain(hashTextContent("console.log('foo');")); + expect(csps[0]).toContain(hashTextContent("console.log('bar');")); + // Loader script for main.js and main2.js appear after 'foo' and before 'bar'. + expect(result).toMatch( + // eslint-disable-next-line max-len + /console.log\('foo'\);<\/script>\s* + + +
Some text
+ + + `); + + const csps = getCsps(result); + expect(csps.length).toBe(1); + expect(csps[0]).toMatch(ONE_HASH_CSP); + + expect(result).toContain( + // eslint-disable-next-line max-len + `document.lastElementChild.appendChild`, + ); + // Head loader script is in the head. + expect(result).toContain(``); + // Only one loader script is created. + expect(Array.from(result.matchAll(/ + + + + + `); + + 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..cf4cb9e3c03e --- /dev/null +++ b/packages/angular/build/src/utils/load-proxy-config.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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; + } + default: { + try { + proxyConfiguration = await import(proxyPath); + } catch (e) { + assertIsError(e); + if (e.code !== 'ERR_REQUIRE_ASYNC_MODULE') { + throw e; + } + + proxyConfiguration = await loadEsmModule<{ default: unknown }>(pathToFileURL(proxyPath)); + } + + break; + } + } + + if ('default' in proxyConfiguration) { + proxyConfiguration = proxyConfiguration.default; + } + + 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..e202273dc73d --- /dev/null +++ b/packages/angular/build/src/utils/load-translations.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 type { Diagnostics } from '@angular/localize/tools'; +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; + +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 import('@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/path.ts b/packages/angular/build/src/utils/path.ts new file mode 100644 index 000000000000..036dcb23502e --- /dev/null +++ b/packages/angular/build/src/utils/path.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 { posix } from 'node:path'; +import { platform } from 'node:process'; + +const WINDOWS_PATH_SEPERATOR_REGEXP = /\\/g; + +/** + * Converts a Windows-style file path to a POSIX-compliant path. + * + * This function replaces all backslashes (`\`) with forward slashes (`/`). + * It is a no-op on POSIX systems (e.g., Linux, macOS), as the conversion + * only runs on Windows (`win32`). + * + * @param path - The file path to convert. + * @returns The POSIX-compliant file path. + * + * @example + * ```ts + * // On a Windows system: + * toPosixPath('C:\\Users\\Test\\file.txt'); + * // => 'C:/Users/Test/file.txt' + * + * // On a POSIX system (Linux/macOS): + * toPosixPath('/home/user/file.txt'); + * // => '/home/user/file.txt' + * ``` + */ +export function toPosixPath(path: string): string { + return platform === 'win32' ? path.replace(WINDOWS_PATH_SEPERATOR_REGEXP, posix.sep) : path; +} 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..6f3f1f3671f9 --- /dev/null +++ b/packages/angular/build/src/utils/postcss-configuration.ts @@ -0,0 +1,127 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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< + | { + configPath: string; + config: PostcssConfiguration; + } + | undefined +> { + 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, configPath }; + } + + // 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, configPath }; +} 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..31912d5e9905 --- /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.at(-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..1d0d9df32d30 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +/** + * @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; + +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 readFile(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://'); +} 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..95b2784c6f63 --- /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 { 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 import( + '@angular/ssr/node' as string + )) as typeof import('@angular/ssr/node', { with: { 'resolution-mode': 'import' } }); + + 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..87ca9928a86f --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/load-esm-from-memory.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 type { ApplicationRef, Type } from '@angular/core'; +import type { BootstrapContext } from '@angular/platform-browser'; +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: ((context: BootstrapContext) => 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..b01bff38b58f --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -0,0 +1,226 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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.at(-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: string[] = [`${publicPath}${fileName}`]; + + for (const { kind, external, path } of imports) { + if (external || initialFiles.has(path) || kind !== 'import-statement') { + continue; + } + + importedPaths.push(`${publicPath}${path}`); + } + + 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..f33f851f10c4 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -0,0 +1,371 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { toPosixPath } from '../path'; +import { addLeadingSlash, addTrailingSlash, joinUrlParts, stripLeadingSlash } 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'; +import { generateRedirectStaticPage } from './utils'; + +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(toPosixPath(destination))] = 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 = stripLeadingSlash(posix.join(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 = joinUrlParts(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: joinUrlParts(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(); + } +} 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..7ded0550b826 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/render-worker.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 { 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'; +import { generateRedirectStaticPage } from './utils'; + +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) }), + ); + + if (!response) { + return null; + } + + const location = response.headers.get('Location'); + + return location ? generateRedirectStaticPage(location) : response.text(); +} + +async function initialize() { + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + + 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..423a71e83ba5 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/routes-extractor-worker.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 { 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 { + // Load the compiler because `@angular/ssr/node` depends on `@angular/` packages, + // which must be processed by the runtime linker, even if they are not used. + await import('@angular/compiler'); + + 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..c740d4de06c4 --- /dev/null +++ b/packages/angular/build/src/utils/server-rendering/utils.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { createRequestHandler } from '@angular/ssr'; +import type { createNodeRequestHandler } from '@angular/ssr/node' with { 'resolution-mode': 'import' }; + +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; +} + +/** + * 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. + */ +export function generateRedirectStaticPage(url: string): string { + return ` + + + + + Redirecting + + + +
Redirecting to ${url}
+ + +`.trim(); +} 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..1535684f635c --- /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' with { 'resolution-mode': 'import' }; +import * as crypto from 'node:crypto'; +import { existsSync, 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 { toPosixPath } from './path'; + +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('/' + toPosixPath(path.relative(this.base, entryPath))); + } 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('/' + toPosixPath(file.path), async () => file.contents); + } + } + for (const file of assetFiles) { + this.fileReaders.set('/' + toPosixPath(file.destination), () => + 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 }[] }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { Generator } = (await import('@angular/service-worker/config' as any)) as typeof import( + '@angular/service-worker/config', + { with: { 'resolution-mode': 'import' } } + ); + + // Generate the manifest + const generator = new Generator(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..d871d75789d3 --- /dev/null +++ b/packages/angular/build/src/utils/supported-browsers.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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'; + +// The below is replaced by bazel `npm_package`. +const BASELINE_DATE = 'BASELINE-DATE-PLACEHOLDER'; + +export function getSupportedBrowsers( + projectRoot: string, + logger: { warn(message: string): void }, +): string[] { + // Read the browserslist configuration containing Angular's browser support policy. + const angularBrowserslist = browserslist(`baseline widely available on ${getBaselineDate()}`); + + // 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); +} + +function getBaselineDate(): string { + // Unlike `npm_package`, `ts_project` which is used to run unit tests does not support substitutions. + return BASELINE_DATE[0] === 'B' ? '2025-01-01' : BASELINE_DATE; +} diff --git a/packages/angular/build/src/utils/test-files.ts b/packages/angular/build/src/utils/test-files.ts new file mode 100644 index 000000000000..522bb1e778c0 --- /dev/null +++ b/packages/angular/build/src/utils/test-files.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 * as fs from 'node:fs/promises'; +import path from 'node:path'; +import { ResultFile } from '../builders/application/results'; +import { BuildOutputFileType } from '../tools/esbuild/bundler-context'; +import { emitFilesToDisk } from '../tools/esbuild/utils'; + +/** + * Writes a collection of build result files to a specified directory. + * This function handles both in-memory and on-disk files, creating subdirectories + * as needed. + * + * @param files A map of file paths to `ResultFile` objects, representing the build output. + * @param testDir The absolute path to the directory where the files should be written. + */ +export async function writeTestFiles( + files: Record, + testDir: string, +): Promise { + const directoryExists = new Set(); + // Writes the test related output files to disk and ensures the containing directories are present + await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => { + if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) { + return; + } + + const fullFilePath = path.join(testDir, filePath); + + // 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 + await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); + } + }); +} 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..689eac37eab5 --- /dev/null +++ b/packages/angular/build/src/utils/url.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * Removes the trailing slash from a URL if it exists. + * + * @param url - The URL string from which to remove the trailing slash. + * @returns The URL string without a trailing slash. + * + * @example + * ```js + * stripTrailingSlash('path/'); // 'path' + * stripTrailingSlash('/path'); // '/path' + * stripTrailingSlash('/'); // '/' + * stripTrailingSlash(''); // '' + * ``` + */ +export function stripTrailingSlash(url: string): string { + // Check if the last character of the URL is a slash + return url.length > 1 && url.at(-1) === '/' ? url.slice(0, -1) : url; +} + +/** + * Removes the leading slash from a URL if it exists. + * + * @param url - The URL string from which to remove the leading slash. + * @returns The URL string without a leading slash. + * + * @example + * ```js + * stripLeadingSlash('/path'); // 'path' + * stripLeadingSlash('/path/'); // 'path/' + * stripLeadingSlash('/'); // '/' + * stripLeadingSlash(''); // '' + * ``` + */ +export function stripLeadingSlash(url: string): string { + // Check if the first character of the URL is a slash + return url.length > 1 && url[0] === '/' ? url.slice(1) : url; +} + +/** + * Adds a leading slash to a URL if it does not already have one. + * + * @param url - The URL string to which the leading slash will be added. + * @returns The URL string with a leading slash. + * + * @example + * ```js + * addLeadingSlash('path'); // '/path' + * addLeadingSlash('/path'); // '/path' + * ``` + */ +export function addLeadingSlash(url: string): string { + // Check if the URL already starts with a slash + return url[0] === '/' ? url : `/${url}`; +} + +/** + * Adds a trailing slash to a URL if it does not already have one. + * + * @param url - The URL string to which the trailing slash will be added. + * @returns The URL string with a trailing slash. + * + * @example + * ```js + * addTrailingSlash('path'); // 'path/' + * addTrailingSlash('path/'); // 'path/' + * ``` + */ +export function addTrailingSlash(url: string): string { + // Check if the URL already end with a slash + return url.at(-1) === '/' ? url : `${url}/`; +} + +/** + * Joins URL parts into a single URL string. + * + * This function takes multiple URL segments, normalizes them by removing leading + * and trailing slashes where appropriate, and then joins them into a single URL. + * + * @param parts - The parts of the URL to join. Each part can be a string with or without slashes. + * @returns The joined URL string, with normalized slashes. + * + * @example + * ```js + * joinUrlParts('path/', '/to/resource'); // '/path/to/resource' + * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource' + * joinUrlParts('http://localhost/path/', 'to/resource'); // 'http://localhost/path/to/resource' + * joinUrlParts('', ''); // '/' + * ``` + */ +export function joinUrlParts(...parts: string[]): string { + const normalizeParts: string[] = []; + for (const part of parts) { + if (part === '') { + // Skip any empty parts + continue; + } + + let normalizedPart = part; + if (part[0] === '/') { + normalizedPart = normalizedPart.slice(1); + } + if (part.at(-1) === '/') { + normalizedPart = normalizedPart.slice(0, -1); + } + if (normalizedPart !== '') { + normalizeParts.push(normalizedPart); + } + } + + const protocolMatch = normalizeParts.length && /^https?:\/\//.test(normalizeParts[0]); + const joinedParts = normalizeParts.join('/'); + + return protocolMatch ? joinedParts : addLeadingSlash(joinedParts); +} 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..78db4302ef1a --- /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: 4_000, + // Web containers do not support transferable objects with receiveOnMessagePort which + // is used when the Atomics based wait loop is enable. + atomics: process.versions.webcontainer ? 'disabled' : 'sync', + recordTiming: false, + ...options, + }; + + // Enable compile code caching if enabled for the main process (only exists on Node.js v22.8+). + // Skip if running inside Bazel via a RUNFILES environment variable check. The cache does not work + // well with Bazel's hermeticity requirements. + const compileCacheDirectory = process.env['RUNFILES'] ? undefined : getCompileCacheDir?.(); + if (compileCacheDirectory) { + if (typeof piscinaOptions.env === 'object') { + piscinaOptions.env['NODE_COMPILE_CACHE'] = compileCacheDirectory; + } else { + // Default behavior of `env` option is to copy current process values + piscinaOptions.env = { + ...process.env, + 'NODE_COMPILE_CACHE': compileCacheDirectory, + }; + } + } + + super(piscinaOptions); + } +} diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index cc965ed2417c..abed616cd810 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -1,122 +1,109 @@ # 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 +# found in the LICENSE file at https://angular.dev/license -load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test") -load("//tools:ts_json_schema.bzl", "ts_json_schema") +load("@npm//:defs.bzl", "npm_link_all_packages") +load("//tools:defaults.bzl", "jasmine_test", "ng_examples_db", "npm_package", "ts_project") load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema") -load("//tools:defaults.bzl", "ts_library") - -# @external_begin -load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") -load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm") -# @external_end +load("//tools:ts_json_schema.bzl", "ts_json_schema") -licenses(["notice"]) # MIT +licenses(["notice"]) package(default_visibility = ["//visibility:public"]) -ts_library( +npm_link_all_packages() + +genrule( + name = "angular_best_practices", + srcs = [ + "//:node_modules/@angular/core/dir", + ], + outs = ["src/commands/mcp/resources/best-practices.md"], + cmd = """ + cp "$(location //:node_modules/@angular/core/dir)/resources/best-practices.md" $@ + """, +) + +RUNTIME_ASSETS = glob( + include = [ + "bin/**/*", + "src/**/*.md", + "src/**/*.json", + ], + exclude = [ + "lib/config/workspace-schema.json", + ], +) + [ + "//packages/angular/cli:lib/config/schema.json", + "//packages/angular/cli:lib/code-examples.db", + ":angular_best_practices", +] + +ts_project( name = "angular-cli", srcs = glob( - include = ["**/*.ts"], + include = [ + "lib/**/*.ts", + "src/**/*.ts", + ], exclude = [ "**/*_spec.ts", - # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces - "node_modules/**", + "**/testing/**", ], ) + [ - # @external_begin # These files are generated from the JSON schema "//packages/angular/cli:lib/config/workspace-schema.ts", - "//packages/angular/cli:commands/analytics.ts", - "//packages/angular/cli:commands/add.ts", - "//packages/angular/cli:commands/build.ts", - "//packages/angular/cli:commands/deploy.ts", - "//packages/angular/cli:commands/config.ts", - "//packages/angular/cli:commands/doc.ts", - "//packages/angular/cli:commands/e2e.ts", - "//packages/angular/cli:commands/easter-egg.ts", - "//packages/angular/cli:commands/generate.ts", - "//packages/angular/cli:commands/help.ts", - "//packages/angular/cli:commands/lint.ts", - "//packages/angular/cli:commands/new.ts", - "//packages/angular/cli:commands/serve.ts", - "//packages/angular/cli:commands/test.ts", - "//packages/angular/cli:commands/update.ts", - "//packages/angular/cli:commands/version.ts", - "//packages/angular/cli:commands/run.ts", - "//packages/angular/cli:commands/extract-i18n.ts", "//packages/angular/cli:src/commands/update/schematic/schema.ts", - # @external_end ], - data = glob( + 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/@modelcontextprotocol/sdk", + ":node_modules/@yarnpkg/lockfile", + ":node_modules/algoliasearch", + ":node_modules/ini", + ":node_modules/jsonc-parser", + ":node_modules/listr2", + ":node_modules/npm-package-arg", + ":node_modules/pacote", + ":node_modules/parse5-html-rewriting-stream", + ":node_modules/resolve", + ":node_modules/yargs", + ":node_modules/zod", + "//: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/semver", + "//:node_modules/typescript", + ], +) + +ng_examples_db( + name = "cli_example_database", + srcs = glob( include = [ - "bin/**/*", - "**/*.json", - "**/*.md", - ], - exclude = [ - # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces - "node_modules/**", - "cli/lib/config/workspace-schema.json", + "lib/examples/**/*.md", ], - ) + [ - "//packages/angular/cli:lib/config/schema.json", - ], - module_name = "@angular/cli", - # strict_checks = False, - deps = [ - "//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/tasks", - "//packages/angular_devkit/schematics/tools", - "@npm//@angular/core", - "@npm//@types/debug", - "@npm//@types/inquirer", - "@npm//@types/node", - "@npm//@types/npm-package-arg", - "@npm//@types/resolve", - "@npm//@types/semver", - "@npm//@types/uuid", - "@npm//ansi-colors", - "@npm//jsonc-parser", - "@npm//open", - "@npm//ora", - ], + ), + out = "lib/code-examples.db", + path = "packages/angular/cli/lib/examples", ) CLI_SCHEMA_DATA = [ - "//packages/angular_devkit/build_angular:src/app-shell/schema.json", - "//packages/angular_devkit/build_angular:src/browser/schema.json", - "//packages/angular_devkit/build_angular:src/dev-server/schema.json", - "//packages/angular_devkit/build_angular:src/extract-i18n/schema.json", - "//packages/angular_devkit/build_angular:src/karma/schema.json", - "//packages/angular_devkit/build_angular:src/ng-packagr/schema.json", - "//packages/angular_devkit/build_angular:src/protractor/schema.json", - "//packages/angular_devkit/build_angular:src/server/schema.json", - "//packages/angular_devkit/build_angular:src/tslint/schema.json", - "//packages/schematics/angular:app-shell/schema.json", - "//packages/schematics/angular:application/schema.json", - "//packages/schematics/angular:class/schema.json", - "//packages/schematics/angular:component/schema.json", - "//packages/schematics/angular:directive/schema.json", - "//packages/schematics/angular:enum/schema.json", - "//packages/schematics/angular:guard/schema.json", - "//packages/schematics/angular:interceptor/schema.json", - "//packages/schematics/angular:interface/schema.json", - "//packages/schematics/angular:library/schema.json", - "//packages/schematics/angular:module/schema.json", - "//packages/schematics/angular:ng-new/schema.json", - "//packages/schematics/angular:pipe/schema.json", - "//packages/schematics/angular:resolver/schema.json", - "//packages/schematics/angular:service/schema.json", - "//packages/schematics/angular:service-worker/schema.json", - "//packages/schematics/angular:web-worker/schema.json", + "//packages/angular/build:schemas", + "//packages/angular_devkit/build_angular:schemas", + "//packages/schematics/angular:schemas", ] cli_json_schema( @@ -132,194 +119,66 @@ ts_json_schema( data = CLI_SCHEMA_DATA, ) -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 = "deploy_schema", - src = "commands/deploy.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "config_schema", - src = "commands/config.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 = "extract-i18n_schema", - src = "commands/extract-i18n.json", - data = [ - "commands/definitions.json", - ], -) - ts_json_schema( name = "update_schematic_schema", src = "src/commands/update/schematic/schema.json", ) -ts_library( +ts_project( name = "angular-cli_test_lib", testonly = True, srcs = glob( - include = ["**/*_spec.ts"], + include = [ + "**/*_spec.ts", + "**/testing/**", + ], exclude = [ # NB: we need to exclude the nested node_modules that is laid out by yarn workspaces "node_modules/**", ], ), - # strict_checks = False, deps = [ ":angular-cli", - "//packages/angular_devkit/core", - "//packages/angular_devkit/schematics", - "//packages/angular_devkit/schematics/testing", - "@npm//@types/semver", - "@npm//rxjs", + ":node_modules/@angular-devkit/core", + ":node_modules/@angular-devkit/schematics", + ":node_modules/@modelcontextprotocol/sdk", + ":node_modules/yargs", + "//:node_modules/@types/semver", + "//:node_modules/@types/yargs", + "//:node_modules/semver", + "//:node_modules/typescript", ], ) -jasmine_node_test( - name = "angular-cli_test", - srcs = [":angular-cli_test_lib"], +jasmine_test( + name = "test", + data = [":angular-cli_test_lib"], ) -# @external_begin -pkg_npm( - name = "npm_package", - deps = [ - ":angular-cli", - ], +genrule( + name = "license", + srcs = ["//:LICENSE"], + outs = ["LICENSE"], + cmd = "cp $(execpath //:LICENSE) $@", ) -pkg_tar( - name = "npm_package_archive", - srcs = [":npm_package"], - extension = "tar.gz", - strip_prefix = "./npm_package", - tags = ["manual"], +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", + ], ) -# @external_end diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md index 954dcd7d702c..4fa87391f04c 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -1,273 +1,5 @@ -## Angular CLI +# Angular CLI - The CLI tool for Angular. - +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. -[![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 Globally - -```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 which 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 on our [documentation website](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 +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..18d1ed73160c --- /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 and no explicit cache +// location defined. The default cache location does not work well with Bazel's hermeticity requirements. +if (!process.env['RUNFILES'] || process.env['NODE_COMPILE_CACHE']) { + 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 889851add1f7..000000000000 --- a/packages/angular/cli/bin/ng +++ /dev/null @@ -1,47 +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'; -} - -// 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. -// tslint:disable-next-line: no-var-keyword -var version = process.versions.node.split('.').map((part) => Number(part)); -if (version[0] % 2 === 1 && version[0] > 14) { - // Allow new odd numbered releases with a warning (currently v15+) - 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/releases/.', - ); - - require('../lib/init'); -} else if ( - version[0] < 12 || - version[0] === 13 || - (version[0] === 12 && version[1] < 14) || - (version[0] === 14 && version[1] < 15) -) { - // Error and exit if less than 12.14 or 13.x or less than 14.15 - console.error( - 'Node.js version ' + - process.version + - ' detected.\n' + - 'The Angular CLI requires a minimum Node.js version of either v12.14 or v14.15.\n\n' + - 'Please update your Node.js version or visit https://nodejs.org/ for additional instructions.\n', - ); - - process.exitCode = 3; -} else { - 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..e0f5eb36a2ef --- /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. +const [major, minor] = process.versions.node.split('.', 2).map((part) => Number(part)); + +if (major % 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 (major < 20 || (major === 20 && minor < 19) || (major === 22 && minor < 12)) { + // Error and exit if less than 20.19 or 22.12 + console.error( + 'Node.js version ' + + process.version + + ' detected.\n' + + 'The Angular CLI requires a minimum Node.js version of v20.19 or v22.12.\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 b24c319ede10..000000000000 --- a/packages/angular/cli/bin/postinstall/analytics-prompt.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; -// This file is ES6 because it needs to be executed as is. - -if ('NG_CLI_ANALYTICS' in process.env) { - return; -} - -try { - var analytics = require('../../models/analytics'); - - analytics - .hasGlobalAnalyticsConfiguration() - .then((hasGlobalConfig) => { - if (!hasGlobalConfig) { - return analytics.promptGlobalAnalytics(); - } - }) - .catch(() => {}); -} catch (_) {} diff --git a/packages/angular/cli/bin/postinstall/script.js b/packages/angular/cli/bin/postinstall/script.js deleted file mode 100644 index 8c091b8009db..000000000000 --- a/packages/angular/cli/bin/postinstall/script.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -// These should not fail but if they do they should not block installation of the package -try { - require('./analytics-prompt'); -} catch (_) {} diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json deleted file mode 100644 index 0b65947a0647..000000000000 --- a/packages/angular/cli/commands.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "add": "./commands/add.json", - "analytics": "./commands/analytics.json", - "build": "./commands/build.json", - "config": "./commands/config.json", - "deploy": "./commands/deploy.json", - "doc": "./commands/doc.json", - "e2e": "./commands/e2e.json", - "extract-i18n": "./commands/extract-i18n.json", - "make-this-awesome": "./commands/easter-egg.json", - "generate": "./commands/generate.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" -} diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts deleted file mode 100644 index edb3d4fee0d5..000000000000 --- a/packages/angular/cli/commands/add-impl.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { PackageManager } from '../lib/config/workspace-schema'; -import { isPackageNameSafeForAnalytics } from '../models/analytics'; -import { Arguments } from '../models/interface'; -import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command'; -import { colors } from '../utilities/color'; -import { installPackage, installTempPackage } from '../utilities/install-package'; -import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; -import { - NgAddSaveDepedency, - PackageManifest, - fetchPackageManifest, - fetchPackageMetadata, -} from '../utilities/package-metadata'; -import { askConfirmation } from '../utilities/prompt'; -import { Spinner } from '../utilities/spinner'; -import { isTTY } from '../utilities/tty'; -import { Schema as AddCommandSchema } from './add'; - -const npa = require('npm-package-arg'); - -export class AddCommand extends SchematicCommand { - readonly allowPrivateSchematics = true; - - async initialize(options: AddCommandSchema & Arguments) { - if (options.registry) { - return super.initialize({ ...options, packageRegistry: options.registry }); - } else { - return super.initialize(options); - } - } - - async run(options: AddCommandSchema & Arguments) { - await ensureCompatibleNpm(this.context.root); - - 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)) { - let validVersion = false; - const installedVersion = await this.findProjectVersion(packageIdentifier.name); - if (installedVersion) { - if (packageIdentifier.type === 'range') { - validVersion = satisfies(installedVersion, packageIdentifier.fetchSpec); - } else if (packageIdentifier.type === 'version') { - const v1 = valid(packageIdentifier.fetchSpec); - const v2 = valid(installedVersion); - validVersion = v1 !== null && v1 === v2; - } else if (!packageIdentifier.rawSpec) { - validVersion = true; - } - } - - if (validVersion) { - // Already installed so just run schematic - this.logger.info('Skipping installation: Package already installed'); - - return this.executeSchematic(packageIdentifier.name, options['--']); - } - } - - const spinner = new Spinner(); - - spinner.start('Determining package manager...'); - const packageManager = await getPackageManager(this.context.root); - const usingYarn = packageManager === PackageManager.Yarn; - spinner.info(`Using package manager: ${colors.grey(packageManager)}`); - - 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 - spinner.start('Searching for compatible package version...'); - - let packageMetadata; - try { - packageMetadata = await fetchPackageMetadata(packageIdentifier.name, this.logger, { - registry: options.registry, - usingYarn, - verbose: options.verbose, - }); - } catch (e) { - spinner.fail('Unable to load package information from registry: ' + 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'); - const semverOptions = { includePrerelease: true }; - - if ( - version && - ((validRange(version) && intersects(version, '7', semverOptions)) || - (valid(version) && satisfies(version, '7', semverOptions))) - ) { - packageIdentifier = npa.resolve('@angular/pwa', '0.12'); - } - } else { - packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); - } - spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); - } else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) { - // 'latest' is invalid so search for most recent matching package - const versionManifests = Object.values(packageMetadata.versions).filter( - (value: PackageManifest) => !prerelease(value.version) && !value.deprecated, - ); - - 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) { - spinner.warn("Unable to find compatible package. Using 'latest'."); - } else { - packageIdentifier = newIdentifier; - spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); - } - } else { - packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); - spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); - } - } - - let collectionName = packageIdentifier.name; - let savePackage: NgAddSaveDepedency | undefined; - - try { - spinner.start('Loading package information from registry...'); - const manifest = await fetchPackageManifest(packageIdentifier, this.logger, { - registry: options.registry, - verbose: options.verbose, - usingYarn, - }); - - savePackage = manifest['ng-add']?.save; - collectionName = manifest.name; - - if (await this.hasMismatchedPeer(manifest)) { - spinner.warn('Package has unmet peer dependencies. Adding the package may not succeed.'); - } else { - spinner.succeed(`Package information loaded.`); - } - } catch (e) { - spinner.fail(`Unable to fetch package information for '${packageIdentifier}': ${e.message}`); - - return 1; - } - - if (!options.skipConfirmation) { - const confirmationResponse = await askConfirmation( - `\nThe package ${colors.blue(packageIdentifier.raw)} will be installed and executed.\n` + - 'Would you like to proceed?', - true, - false, - ); - - if (!confirmationResponse) { - if (!isTTY) { - this.logger.error( - 'No terminal detected. ' + - `'--skip-confirmation' can be used to bypass installation confirmation. ` + - `Ensure package name is correct prior to '--skip-confirmation' option usage.`, - ); - } - this.logger.error('Command aborted.'); - - return 1; - } - } - - if (savePackage === false) { - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const { status, tempNodeModules } = await installTempPackage( - packageIdentifier.raw, - packageManager, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); - const resolvedCollectionPath = require.resolve(join(collectionName, 'package.json'), { - paths: [tempNodeModules], - }); - - if (status !== 0) { - return status; - } - - collectionName = dirname(resolvedCollectionPath); - } else { - const status = await installPackage( - packageIdentifier.raw, - packageManager, - savePackage, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); - - if (status !== 0) { - return status; - } - } - - 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.context.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: RunSchematicOptions = { - schematicOptions: options, - collectionName, - schematicName: 'ng-add', - 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.context.root], - }); - } catch {} - - if (installedPackage) { - try { - const installed = await fetchPackageManifest(dirname(installedPackage), this.logger); - - return installed.version; - } catch {} - } - - let projectManifest; - try { - projectManifest = await fetchPackageManifest(this.context.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; - } - - 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/commands/add.json b/packages/angular/cli/commands/add.json deleted file mode 100644 index 99cd82d897fb..000000000000 --- a/packages/angular/cli/commands/add.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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" - } - ] - }, - "verbose": { - "description": "Display additional details about internal operations during execution.", - "type": "boolean", - "default": false - }, - "skipConfirmation": { - "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 - } - }, - "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 09cd2e239d76..000000000000 --- a/packages/angular/cli/commands/add.md +++ /dev/null @@ -1,10 +0,0 @@ -Adds the npm package for a published library to your workspace, and configures -the project in the current working directory (or the default project if you are -not in a project 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 -``` - -The default 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 b0cc575ad173..000000000000 --- a/packages/angular/cli/commands/analytics-impl.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { Schema as AnalyticsCommandSchema, ProjectSetting, 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(); - - 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(); - - return 3; - } - break; - - case SettingOrProject.Prompt: - await promptGlobalAnalytics(true); - break; - - default: - await this.printHelp(); - - return 4; - } - } catch (err) { - this.logger.fatal(err.message); - - return 1; - } - - return 0; - } -} diff --git a/packages/angular/cli/commands/analytics-long.md b/packages/angular/cli/commands/analytics-long.md deleted file mode 100644 index 87b9925d1473..000000000000 --- a/packages/angular/cli/commands/analytics-long.md +++ /dev/null @@ -1,8 +0,0 @@ -The value of _settingOrProject_ is one of the following. - -- "on" : Enables analytics gathering and reporting for the user. -- "off" : Disables analytics gathering and reporting for the user. -- "ci" : Enables analytics and configures reporting for use with Continuous Integration, - which uses a common CI user. -- "prompt" : Prompts the user to set the status interactively. -- "project" : Sets the default status for the project to the _projectSetting_ value, which can be any of the other values. The _projectSetting_ argument is ignored for all other values of _settingOrProject_. diff --git a/packages/angular/cli/commands/analytics.json b/packages/angular/cli/commands/analytics.json deleted file mode 100644 index ee2612b20399..000000000000 --- a/packages/angular/cli/commands/analytics.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/analytics.json", - "description": "Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.", - "$longDescription": "./analytics-long.md", - - "$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 b3ff226bb2cd..000000000000 --- a/packages/angular/cli/commands/build-impl.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 BuildCommandSchema } from './build'; - -export class BuildCommand extends ArchitectCommand { - public readonly target = 'build'; - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build-long.md deleted file mode 100644 index 57bf9a16edd4..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 "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 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 df9d93b85a19..000000000000 --- a/packages/angular/cli/commands/build.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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" } - ] -} diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts deleted file mode 100644 index 7e7b242ac926..000000000000 --- a/packages/angular/cli/commands/config-impl.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { JsonValue, tags } from '@angular-devkit/core'; -import { v4 as uuidV4 } from 'uuid'; -import { Command } from '../models/command'; -import { Arguments, CommandScope } from '../models/interface'; -import { getWorkspaceRaw, migrateLegacyGlobalConfig, validateWorkspace } from '../utilities/config'; -import { JSONFile, parseJson } from '../utilities/json-file'; -import { Schema as ConfigCommandSchema } from './config'; - -const validCliPaths = new Map< - string, - ((arg: string | number | boolean | undefined) => string) | undefined ->([ - ['cli.warnings.versionMismatch', undefined], - ['cli.defaultCollection', undefined], - ['cli.packageManager', undefined], - - ['cli.analytics', undefined], - ['cli.analyticsSharing.tracking', undefined], - ['cli.analyticsSharing.uuid', (v) => (v ? `${v}` : uuidV4())], -]); - -/** - * 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 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; - } - - return parseJson(valueString) ?? value ?? undefined; -} - -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] = getWorkspaceRaw(level); - - if (options.global && !config) { - try { - if (migrateLegacyGlobalConfig()) { - config = getWorkspaceRaw(level)[0]; - 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, options); - } else { - return this.set(options); - } - } - - private get(jsonFile: JSONFile, options: ConfigCommandSchema) { - let value; - if (options.jsonPath) { - value = jsonFile.get(parseJsonPath(options.jsonPath)); - } else { - value = jsonFile.content; - } - - if (value === undefined) { - this.logger.error('Value cannot be found.'); - - return 1; - } else if (typeof value === 'string') { - this.logger.info(value); - } else { - this.logger.info(JSON.stringify(value, null, 2)); - } - - return 0; - } - - private async set(options: ConfigCommandSchema) { - if (!options.jsonPath?.trim()) { - throw new Error('Invalid Path.'); - } - - if ( - options.global && - !options.jsonPath.startsWith('schematics.') && - !validCliPaths.has(options.jsonPath) - ) { - throw new Error('Invalid Path.'); - } - - const [config, configPath] = getWorkspaceRaw(options.global ? 'global' : 'local'); - if (!config || !configPath) { - this.logger.error('Confguration file cannot be found.'); - - return 1; - } - - const jsonPath = parseJsonPath(options.jsonPath); - const value = validCliPaths.get(options.jsonPath)?.(options.value) ?? options.value; - const modified = config.modify(jsonPath, normalizeValue(value)); - - if (!modified) { - this.logger.error('Value cannot be found.'); - - return 1; - } - - try { - await validateWorkspace(parseJson(config.content)); - } catch (error) { - this.logger.fatal(error.message); - - return 1; - } - - config.save(); - - 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 7f44f63b3b32..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). diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json deleted file mode 100644 index bec13fca4c0f..000000000000 --- a/packages/angular/cli/commands/config.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "Access 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 a18355349f46..000000000000 --- a/packages/angular/cli/commands/definitions.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.\nSetting this explicitly overrides the \"--prod\" flag.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Shorthand for \"--configuration=production\".\nSet 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", - "x-deprecated": "Use `--configuration production` instead." - } - } - }, - "base": { - "type": "object", - "properties": { - "help": { - "enum": [true, false, "json", "JSON"], - "description": "Shows a help message for this command in the console.", - "default": false - } - } - }, - "schematic": { - "type": "object", - "properties": { - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through and reports activity without writing out results." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Force overwriting of existing files." - } - } - }, - "interactive": { - "type": "object", - "properties": { - "interactive": { - "type": "boolean", - "default": "true", - "description": "Enable interactive input prompts." - }, - "defaults": { - "type": "boolean", - "default": "false", - "description": "Disable interactive input prompts for options with a default." - } - } - } - } -} diff --git a/packages/angular/cli/commands/deploy-impl.ts b/packages/angular/cli/commands/deploy-impl.ts deleted file mode 100644 index c5c043796fc7..000000000000 --- a/packages/angular/cli/commands/deploy-impl.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 DeployCommandSchema } from './deploy'; - -const BuilderMissing = ` -Cannot find "deploy" target for the specified project. - -You should add a package that implements deployment capabilities for your -favorite platform. - -For example: - ng add @angular/fire - ng add @azure/ng-deploy - ng add @zeit/ng-deploy - -Find more packages on npm https://www.npmjs.com/search?q=ng%20deploy -`; - -export class DeployCommand extends ArchitectCommand { - public readonly target = 'deploy'; - public readonly missingTargetError = BuilderMissing; - - public async initialize(options: DeployCommandSchema & Arguments): Promise { - if (!options.help) { - return super.initialize(options); - } - } -} diff --git a/packages/angular/cli/commands/deploy-long.md b/packages/angular/cli/commands/deploy-long.md deleted file mode 100644 index 9d13ad2a9890..000000000000 --- a/packages/angular/cli/commands/deploy-long.md +++ /dev/null @@ -1,22 +0,0 @@ -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](guide/cli-builder). -For example: - -```json -"projects": { - "my-project": { - ... - "architect": { - ... - "deploy": { - "builder": "@angular/fire:deploy", - "options": {} - } - } - } -} -``` diff --git a/packages/angular/cli/commands/deploy.json b/packages/angular/cli/commands/deploy.json deleted file mode 100644 index cc7c860dde1c..000000000000 --- a/packages/angular/cli/commands/deploy.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/deploy.json", - "description": "Invokes the deploy builder for a specified project or for the default project in the workspace.", - "$longDescription": "./deploy-long.md", - - "$scope": "in", - "$type": "architect", - "$impl": "./deploy-impl#DeployCommand", - - "allOf": [ - { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to deploy.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [] - }, - { - "$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 6837add727e9..000000000000 --- a/packages/angular/cli/commands/doc-impl.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 open from 'open'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { Schema as DocCommandSchema } from './doc'; - -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 { - /* eslint-disable-next-line import/no-extraneous-dependencies */ - const currentNgVersion = (await import('@angular/core')).VERSION.major; - domain = `v${currentNgVersion}.angular.io`; - } catch (e) {} - } - - let searchUrl = `https://${domain}/api?query=${options.keyword}`; - - if (options.search) { - searchUrl = `https://${domain}/docs?search=${options.keyword}`; - } - - await 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 bb01549c6099..000000000000 --- a/packages/angular/cli/commands/doc.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "Search 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 7ef411d7931f..000000000000 --- a/packages/angular/cli/commands/e2e-impl.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 readonly missingTargetError = ` -Cannot find "e2e" target for the specified project. - -You should add a package that implements end-to-end testing capabilities. - -For example: - Cypress: ng add @cypress/schematic - WebdriverIO: ng add @wdio/schematics - -More options will be added to the list as they become available. -`; - - async initialize(options: E2eCommandSchema & Arguments) { - if (!options.help) { - return super.initialize(options); - } - } -} diff --git a/packages/angular/cli/commands/e2e-long.md b/packages/angular/cli/commands/e2e-long.md deleted file mode 100644 index 369b0c71e443..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. diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json deleted file mode 100644 index a8c8cccc4b62..000000000000 --- a/packages/angular/cli/commands/e2e.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/e2e.json", - "description": "Builds and serves an Angular app, then runs end-to-end tests.", - "$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 3857c38444a5..000000000000 --- a/packages/angular/cli/commands/easter-egg-impl.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 79d9e1bb2edf..000000000000 --- a/packages/angular/cli/commands/easter-egg.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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/extract-i18n-impl.ts b/packages/angular/cli/commands/extract-i18n-impl.ts deleted file mode 100644 index 5b3709592e25..000000000000 --- a/packages/angular/cli/commands/extract-i18n-impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 ExtractI18nCommandSchema } from './extract-i18n'; - -export class ExtractI18nCommand extends ArchitectCommand { - public readonly target = 'extract-i18n'; - - public async run(options: ExtractI18nCommandSchema & 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; - } - - const commandName = process.argv[2]; - if (['xi18n', 'i18n-extract'].includes(commandName)) { - this.logger.warn( - `Warning: "ng ${commandName}" has been deprecated and will be removed in a future major version. ` + - 'Please use "ng extract-i18n" instead.', - ); - } - - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/extract-i18n.json b/packages/angular/cli/commands/extract-i18n.json deleted file mode 100644 index 2010fa899190..000000000000 --- a/packages/angular/cli/commands/extract-i18n.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/extract-i18n.json", - "description": "Extracts i18n messages from source code.", - "$longDescription": "", - - "$aliases": ["i18n-extract", "xi18n"], - "$scope": "in", - "$type": "architect", - "$impl": "./extract-i18n-impl#ExtractI18nCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$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 ca893d5888d8..000000000000 --- a/packages/angular/cli/commands/generate-impl.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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] = await 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 ((await 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(); - } - - 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 { - if (!this.collectionName || !this.schematicName) { - return; - } - const escapedSchematicName = (this.longSchematicName || this.schematicName).replace(/\//g, '_'); - - return super.reportAnalytics( - ['generate', this.collectionName.replace(/\//g, '_'), escapedSchematicName], - options, - ); - } - - private async parseSchematicInfo( - options: GenerateCommandSchema, - ): Promise<[string, string | undefined]> { - let collectionName = await this.getDefaultSchematicCollection(); - - let schematicName = options.schematic; - - if (schematicName && schematicName.includes(':')) { - [collectionName, schematicName] = schematicName.split(':', 2); - } - - return [collectionName, schematicName]; - } - - public async printHelp() { - await super.printHelp(); - - 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 53228340abd4..000000000000 --- a/packages/angular/cli/commands/generate.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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 c7ccc282493d..000000000000 --- a/packages/angular/cli/commands/help-impl.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 cc4b790f906e..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 a6513118d0e4..000000000000 --- a/packages/angular/cli/commands/help.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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 dd28bc1dcdd6..000000000000 --- a/packages/angular/cli/commands/lint-impl.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 LintCommandSchema } from './lint'; - -const MissingBuilder = ` -Cannot find "lint" target for the specified project. - -You should add a package that implements linting capabilities. - -For example: - ng add @angular-eslint/schematics -`; - -export class LintCommand extends ArchitectCommand { - readonly target = 'lint'; - readonly multiTarget = true; - readonly missingTargetError = MissingBuilder; - - async initialize(options: LintCommandSchema & Arguments): Promise { - if (!options.help) { - return super.initialize(options); - } - } -} diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint-long.md deleted file mode 100644 index d6a4ee0f79b8..000000000000 --- a/packages/angular/cli/commands/lint-long.md +++ /dev/null @@ -1,20 +0,0 @@ -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 the default project. - -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](guide/cli-builder). -For example: - -```json -"projects": { - "my-project": { - ... - "architect": { - ... - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": {} - } - } - } -} -``` diff --git a/packages/angular/cli/commands/lint.json b/packages/angular/cli/commands/lint.json deleted file mode 100644 index 824632e79f76..000000000000 --- a/packages/angular/cli/commands/lint.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "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 53646a1296d4..000000000000 --- a/packages/angular/cli/commands/new-impl.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 } 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) { - this.collectionName = options.collection || (await this.getDefaultSchematicCollection()); - - 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, - }); - } -} diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json deleted file mode 100644 index 90efa76056be..000000000000 --- a/packages/angular/cli/commands/new.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/new.json", - "description": "Creates a new workspace and an initial Angular application.", - "$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 application." - }, - "verbose": { - "type": "boolean", - "default": false, - "aliases": ["v"], - "description": "Add 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 0d8699958041..000000000000 --- a/packages/angular/cli/commands/new.md +++ /dev/null @@ -1,16 +0,0 @@ -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. Corresponding end-to-end tests are placed in the `e2e/` 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 setting the `--createApplication` option to false. -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/commands/run-impl.ts b/packages/angular/cli/commands/run-impl.ts deleted file mode 100644 index 3a0968e55898..000000000000 --- a/packages/angular/cli/commands/run-impl.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 65a307fcd771..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] -``` diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json deleted file mode 100644 index f4e2287dbf35..000000000000 --- a/packages/angular/cli/commands/run.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "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 4daaeb3d3c88..000000000000 --- a/packages/angular/cli/commands/serve-impl.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 ServeCommandSchema } from './serve'; - -export class ServeCommand extends ArchitectCommand { - public readonly target = 'serve'; - - public validate() { - return true; - } - - public async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/serve.json b/packages/angular/cli/commands/serve.json deleted file mode 100644 index efc7ba4089ae..000000000000 --- a/packages/angular/cli/commands/serve.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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" } - ] -} diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts deleted file mode 100644 index 71d7b9147b36..000000000000 --- a/packages/angular/cli/commands/test-impl.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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.json b/packages/angular/cli/commands/test.json deleted file mode 100644 index 5fb4ce014c48..000000000000 --- a/packages/angular/cli/commands/test.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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 2c9cea0eb2c8..000000000000 --- a/packages/angular/cli/commands/update-impl.ts +++ /dev/null @@ -1,941 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics'; -import { NodeWorkflow } from '@angular-devkit/schematics/tools'; -import { execSync } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as semver from 'semver'; -import { PackageManager } from '../lib/config/workspace-schema'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { SchematicEngineHost } from '../models/schematic-engine-host'; -import { colors } from '../utilities/color'; -import { installAllPackages, runTempPackageBin } from '../utilities/install-package'; -import { writeErrorToLogFile } from '../utilities/log-file'; -import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; -import { - PackageIdentifier, - PackageManifest, - PackageMetadata, - fetchPackageManifest, - fetchPackageMetadata, -} from '../utilities/package-metadata'; -import { - PackageTreeNode, - findPackageJson, - getProjectDependencies, - readPackageJson, -} from '../utilities/package-tree'; -import { Schema as UpdateCommandSchema } from './update'; - -const npa = require('npm-package-arg') as (selector: string) => PackageIdentifier; -const pickManifest = require('npm-pick-manifest') as ( - metadata: PackageMetadata, - selector: string, -) => PackageManifest; - -const NG_VERSION_9_POST_MSG = colors.cyan( - '\nYour project has been updated to Angular version 9!\n' + - 'For more info, please see: https://v9.angular.io/guide/updating-to-version-9', -); - -const UPDATE_SCHEMATIC_COLLECTION = path.join( - __dirname, - '../src/commands/update/schematic/collection.json', -); - -/** - * Disable CLI version mismatch checks and forces usage of the invoked CLI - * instead of invoking the local installed version. - */ -const disableVersionCheckEnv = process.env['NG_DISABLE_VERSION_CHECK']; -const disableVersionCheck = - disableVersionCheckEnv !== undefined && - disableVersionCheckEnv !== '0' && - disableVersionCheckEnv.toLowerCase() !== 'false'; - -export class UpdateCommand extends Command { - public readonly allowMissingWorkspace = true; - private workflow!: NodeWorkflow; - private packageManager = PackageManager.Npm; - - async initialize(options: UpdateCommandSchema & Arguments) { - this.packageManager = await getPackageManager(this.context.root); - this.workflow = new NodeWorkflow(this.context.root, { - packageManager: this.packageManager, - packageManagerForce: options.force, - // __dirname -> favor @schematics/update from this package - // Otherwise, use packages from the active workspace (migrations) - resolvePaths: [__dirname, this.context.root], - schemaValidation: true, - engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), - }); - } - - private async executeSchematic( - collection: string, - schematic: string, - options = {}, - ): Promise<{ success: boolean; files: Set }> { - let error = false; - let logs: string[] = []; - const files = new Set(); - - const reporterSubscription = this.workflow.reporter.subscribe((event) => { - // 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.error(`ERROR! ${eventPath} ${desc}.`); - 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': - const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to; - logs.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`); - files.add(eventPath); - break; - } - }); - - const lifecycleSubscription = this.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) => this.logger.info(` ${log}`)); - logs = []; - } - } - }); - - // TODO: Allow passing a schematic instance directly - try { - await this.workflow - .execute({ - collection, - schematic, - options, - logger: this.logger, - }) - .toPromise(); - - reporterSubscription.unsubscribe(); - lifecycleSubscription.unsubscribe(); - - return { success: !error, files }; - } catch (e) { - if (e instanceof UnsuccessfulWorkflowExecution) { - this.logger.error( - `${colors.symbols.cross} Migration failed. See above for further details.\n`, - ); - } else { - const logPath = writeErrorToLogFile(e); - this.logger.fatal( - `${colors.symbols.cross} Migration failed: ${e.message}\n` + - ` See "${logPath}" for further details.\n`, - ); - } - - return { success: false, files }; - } - } - - /** - * @return Whether or not the migration was performed successfully. - */ - private async executeMigration( - packageName: string, - collectionPath: string, - migrationName: string, - commit?: boolean, - ): Promise { - const collection = this.workflow.engine.createCollection(collectionPath); - const name = collection.listSchematicNames().find((name) => name === migrationName); - if (!name) { - this.logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`); - - return false; - } - - const schematic = this.workflow.engine.createSchematic(name, collection); - - this.logger.info( - colors.cyan(`** Executing '${migrationName}' of package '${packageName}' **\n`), - ); - - return this.executePackageMigrations([schematic.description], packageName, commit); - } - - /** - * @return Whether or not the migrations were performed successfully. - */ - private async executeMigrations( - packageName: string, - collectionPath: string, - from: string, - to: string, - commit?: boolean, - ): Promise { - const collection = this.workflow.engine.createCollection(collectionPath); - const migrationRange = new semver.Range( - '>' + (semver.prerelease(from) ? from.split('-')[0] + '-0' : from) + ' <=' + to, - ); - const migrations = []; - - for (const name of collection.listSchematicNames()) { - const schematic = this.workflow.engine.createSchematic(name, collection); - const description = schematic.description as typeof schematic.description & { - version?: string; - }; - description.version = coerceVersionNumber(description.version) || undefined; - if (!description.version) { - continue; - } - - if (semver.satisfies(description.version, migrationRange, { includePrerelease: true })) { - migrations.push(description as typeof schematic.description & { version: string }); - } - } - - migrations.sort((a, b) => semver.compare(a.version, b.version) || a.name.localeCompare(b.name)); - - if (migrations.length === 0) { - return true; - } - - this.logger.info(colors.cyan(`** Executing migrations of package '${packageName}' **\n`)); - - return this.executePackageMigrations(migrations, packageName, commit); - } - - private async executePackageMigrations( - migrations: Iterable<{ name: string; description: string; collection: { name: string } }>, - packageName: string, - commit = false, - ): Promise { - for (const migration of migrations) { - const [title, ...description] = migration.description.split('. '); - - this.logger.info( - colors.cyan(colors.symbols.pointer) + - ' ' + - colors.bold(title.endsWith('.') ? title : title + '.'), - ); - - if (description.length) { - this.logger.info(' ' + description.join('.\n ')); - } - - const result = await this.executeSchematic(migration.collection.name, migration.name); - if (!result.success) { - return false; - } - - this.logger.info(' Migration completed.'); - - // 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 false; - } - } - - this.logger.info(''); // Extra trailing newline. - } - - return true; - } - - // eslint-disable-next-line max-lines-per-function - async run(options: UpdateCommandSchema & Arguments) { - await ensureCompatibleNpm(this.context.root); - - // Check if the current installed CLI version is older than the latest version. - if (!disableVersionCheck && (await this.checkCLILatestVersion(options.verbose, options.next))) { - this.logger.warn( - `The installed local Angular CLI version is older than the latest ${ - options.next ? 'pre-release' : 'stable' - } version.\n` + 'Installing a temporary version to perform the update.', - ); - - return runTempPackageBin( - `@angular/cli@${options.next ? 'next' : 'latest'}`, - this.packageManager, - process.argv.slice(2), - ); - } - - const logVerbose = (message: string) => { - if (options.verbose) { - this.logger.info(message); - } - }; - - if (options.all) { - const updateCmd = - this.packageManager === PackageManager.Yarn - ? `'yarn upgrade-interactive' or 'yarn upgrade'` - : `'${this.packageManager} update'`; - - this.logger.warn(` - '--all' functionality has been removed as updating multiple packages at once is not recommended. - To update packages which don’t provide 'ng update' capabilities in your workspace 'package.json' use ${updateCmd} instead. - Run the package manager update command after updating packages which provide 'ng update' capabilities. - `); - - return 0; - } - - const packages: PackageIdentifier[] = []; - for (const request of options['--'] || []) { - try { - const 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 (options.migrateOnly && packageIdentifier.rawSpec) { - this.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); - } catch (e) { - this.logger.error(e.message); - - return 1; - } - } - - 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. - if (packages.length && !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; - } - } - - this.logger.info(`Using package manager: '${this.packageManager}'`); - this.logger.info('Collecting installed dependencies...'); - - const rootDependencies = await getProjectDependencies(this.context.root); - - this.logger.info(`Found ${rootDependencies.size} dependencies.`); - - if (packages.length === 0) { - // Show status - const { success } = await this.executeSchematic(UPDATE_SCHEMATIC_COLLECTION, 'update', { - force: options.force || false, - next: options.next || false, - verbose: options.verbose || false, - packageManager: this.packageManager, - packages: [], - }); - - return success ? 0 : 1; - } - - if (options.migrateOnly) { - if (!options.from && typeof options.migrateOnly !== 'string') { - this.logger.error( - '"from" option is required when using the "migrate-only" option without a migration name.', - ); - - 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; - const packageDependency = rootDependencies.get(packageName); - let packagePath = packageDependency?.path; - let packageNode = packageDependency?.package; - if (packageDependency && !packageNode) { - this.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) { - this.logger.error('Package is not installed.'); - - return 1; - } - - const updateMetadata = packageNode['ng-update']; - let migrations = 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(packagePath, 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: [packagePath] }); - } 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; - } - } - - let success = false; - if (typeof options.migrateOnly == 'string') { - success = await this.executeMigration( - packageName, - migrations, - options.migrateOnly, - options.createCommits, - ); - } else { - const from = coerceVersionNumber(options.from); - if (!from) { - this.logger.error(`"from" value [${options.from}] is not a valid version.`); - - return 1; - } - - success = await this.executeMigrations( - packageName, - migrations, - from, - options.to || packageNode.version, - options.createCommits, - ); - } - - if (success) { - if ( - packageName === '@angular/core' && - options.from && - +options.from.split('.')[0] < 9 && - (options.to || packageNode.version).split('.')[0] === '9' - ) { - this.logger.info(NG_VERSION_9_POST_MSG); - } - - return 0; - } - - return 1; - } - - 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) { - 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' && 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, { - verbose: options.verbose, - }); - } 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' || - requestIdentifier.type === 'range' || - requestIdentifier.type === 'tag' - ) { - try { - manifest = pickManifest(metadata, requestIdentifier.fetchSpec); - } catch (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) { - if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') { - throw e; - } - } - } - } else if (e.code !== 'ENOVERSIONS') { - throw e; - } - } - } - - if (!manifest) { - this.logger.error( - `Package specified by '${requestIdentifier.raw}' does not exist within the registry.`, - ); - - return 1; - } - - if (node.package?.name === '@angular/cli') { - // Migrations for non LTS versions of Angular CLI are no longer included in @schematics/angular v12. - const toBeInstalledMajorVersion = +manifest.version.split('.')[0]; - const currentMajorVersion = +node.package.version.split('.')[0]; - if (currentMajorVersion < 9 && toBeInstalledMajorVersion >= 12) { - const updateVersions: Record = { - 1: 6, - 6: 7, - 7: 8, - 8: 9, - }; - - const updateTo = updateVersions[currentMajorVersion]; - this.logger.error( - 'Updating multiple major versions at once is not recommended. ' + - `Run 'ng update @angular/cli@${updateTo}' in your workspace directory ` + - `to update to latest '${updateTo}.x' version of '@angular/cli'.\n\n` + - 'For more information about the update process, see https://update.angular.io/.', - ); - - return 1; - } - } - - if (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; - } - - const { success } = await this.executeSchematic(UPDATE_SCHEMATIC_COLLECTION, 'update', { - verbose: options.verbose || false, - force: options.force || false, - next: !!options.next, - packageManager: this.packageManager, - packages: packagesToUpdate, - }); - - if (success) { - try { - // Remove existing node modules directory to provide a stronger guarantee that packages - // will be hoisted into the correct locations. - await fs.promises.rmdir(path.join(this.context.root, 'node_modules'), { - recursive: true, - maxRetries: 3, - }); - } catch {} - - const result = await installAllPackages( - this.packageManager, - options.force ? ['--force'] : [], - this.context.root, - ); - if (result !== 0) { - return result; - } - } - - if (success && options.createCommits) { - const committed = this.commit( - `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`, - ); - if (!committed) { - 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) { - 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 - require.resolve(path.join(migration.package, 'package.json'), { - paths: [this.context.root], - }), - ); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - // Fallback to trying to resolve the package's main entry point - packagePath = require.resolve(migration.package, { paths: [this.context.root] }); - } else { - throw e; - } - } - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - logVerbose(e.toString()); - this.logger.error( - `Migrations for package (${migration.package}) were not found.` + - ' The package could not be found in the workspace.', - ); - } else { - this.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 (fs.existsSync(localMigrations)) { - migrations = localMigrations; - } else { - // Try to resolve from package location. - // This avoids issues with package hoisting. - try { - migrations = require.resolve(migration.collection, { paths: [packagePath] }); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - this.logger.error(`Migrations for package (${migration.package}) were not found.`); - } else { - this.logger.error( - `Unable to resolve migrations for package (${migration.package}). [${e.message}]`, - ); - } - - return 1; - } - } - const result = await this.executeMigrations( - migration.package, - migrations, - migration.from, - migration.to, - options.createCommits, - ); - - if (!result) { - return 0; - } - } - - if ( - migrations.some( - (m) => - m.package === '@angular/core' && - m.to.split('.')[0] === '9' && - +m.from.split('.')[0] < 9, - ) - ) { - this.logger.info(NG_VERSION_9_POST_MSG); - } - } - - return success ? 0 : 1; - } - - /** - * @return Whether or not the commit was successful. - */ - private commit(message: string): boolean { - // Check if a commit is needed. - let commitNeeded: boolean; - try { - commitNeeded = hasChangesToCommit(); - } catch (err) { - this.logger.error(` Failed to read Git tree:\n${err.stderr}`); - - return false; - } - - if (!commitNeeded) { - this.logger.info(' No changes to commit after migration.'); - - return true; - } - - // Commit changes and abort on error. - try { - createCommit(message); - } catch (err) { - this.logger.error(`Failed to commit update (${message}):\n${err.stderr}`); - - return false; - } - - // Notify user of the commit. - const hash = findCurrentGitSha(); - const shortMessage = message.split('\n')[0]; - if (hash) { - this.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. - this.logger.info(` Committed migration step: ${shortMessage}.`); - this.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 than the latest version. - * @returns `true` when the installed version is older. - */ - private async checkCLILatestVersion(verbose = false, next = false): Promise { - const { version: installedCLIVersion } = require('../package.json'); - - const LatestCLIManifest = await fetchPackageManifest( - `@angular/cli@${next ? 'next' : 'latest'}`, - this.logger, - { - verbose, - usingYarn: this.packageManager === PackageManager.Yarn, - }, - ); - - return semver.lt(installedCLIVersion, LatestCLIManifest.version); - } -} - -/** - * @return Whether or not the working directory has Git changes to commit. - */ -function hasChangesToCommit(): boolean { - // List all modified files not covered by .gitignore. - const files = execSync('git ls-files -m -d -o --exclude-standard').toString(); - - // If any files are returned, then there must be something to commit. - return files !== ''; -} - -/** - * 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 { - const hash = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }); - - return hash.trim(); - } catch { - return null; - } -} - -function getShortHash(commitHash: string): string { - return commitHash.slice(0, 9); -} - -function coerceVersionNumber(version: string | undefined): string | null { - if (!version) { - return null; - } - - if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { - const match = version.match(/^\d{1,30}(\.\d{1,30})*/); - - if (!match) { - return null; - } - - if (!match[1]) { - version = version.substr(0, match[0].length) + '.0.0' + version.substr(match[0].length); - } else if (!match[2]) { - version = version.substr(0, match[0].length) + '.0' + version.substr(match[0].length); - } else { - return null; - } - } - - return semver.valid(version); -} diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update-long.md deleted file mode 100644 index 72df66ce35da..000000000000 --- a/packages/angular/cli/commands/update-long.md +++ /dev/null @@ -1,22 +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` 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.io/). diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json deleted file mode 100644 index 7de5a1935146..000000000000 --- a/packages/angular/cli/commands/update.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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": "Ignore peer dependency version mismatches. Passes the `--force` flag to the package manager when installing packages.", - "default": false, - "type": "boolean" - }, - "all": { - "description": "Whether to update all packages in package.json.", - "default": false, - "type": "boolean", - "x-deprecated": true - }, - "next": { - "description": "Use the prerelease version, including beta and RCs.", - "default": false, - "type": "boolean" - }, - "migrateOnly": { - "description": "Only perform a migration, do not update the installed version.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "description": "The name of the migration to run." - } - ] - }, - "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" - }, - "verbose": { - "description": "Display additional details about internal operations during execution.", - "type": "boolean", - "default": false - }, - "createCommits": { - "description": "Create source control commits for updates and migrations.", - "type": "boolean", - "default": false, - "aliases": ["C"] - } - } - } - ] -} diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts deleted file mode 100644 index 757f28e10ade..000000000000 --- a/packages/angular/cli/commands/version-impl.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 path from 'path'; -import { Command } from '../models/command'; -import { colors } from '../utilities/color'; -import { getPackageManager } from '../utilities/package-manager'; -import { Schema as VersionCommandSchema } from './version'; - -/** - * Major versions of Node.js that are officially supported by Angular. - */ -const SUPPORTED_NODE_MAJORS = [12, 14]; - -interface PartialPackageInfo { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; -} - -export class VersionCommand extends Command { - public static aliases = ['v']; - - async run() { - const cliPackage: PartialPackageInfo = require('../package.json'); - let workspacePackage: PartialPackageInfo | undefined; - try { - workspacePackage = require(path.resolve(this.context.root, 'package.json')); - } catch {} - - const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part)); - const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor); - - const patterns = [ - /^@angular\/.*/, - /^@angular-devkit\/.*/, - /^@bazel\/.*/, - /^@ngtools\/.*/, - /^@nguniversal\/.*/, - /^@schematics\/.*/, - /^rxjs$/, - /^typescript$/, - /^ng-packagr$/, - /^webpack$/, - ]; - - const packageNames = [ - ...Object.keys(cliPackage.dependencies || {}), - ...Object.keys(cliPackage.devDependencies || {}), - ...Object.keys(workspacePackage?.dependencies || {}), - ...Object.keys(workspacePackage?.devDependencies || {}), - ]; - - 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); - - return acc; - }, {} as { [module: string]: string }); - - 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 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}${unsupportedNodeVersion ? ' (Unsupported)' : ''} - Package Manager: ${await this.getPackageManager()} - 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) { - this.logger.warn( - `Warning: The current version of Node (${process.versions.node}) is not supported by Angular.`, - ); - } - } - - private getVersion(moduleName: string): string { - let packagePath; - let cliOnly = false; - - // Try to find the package in the workspace - try { - packagePath = require.resolve(`${moduleName}/package.json`, { paths: [this.context.root] }); - } catch {} - - // If not found, try to find within the CLI - if (!packagePath) { - try { - packagePath = require.resolve(`${moduleName}/package.json`); - cliOnly = true; - } catch {} - } - - let version: string | undefined; - - // If found, attempt to get the version - if (packagePath) { - try { - version = require(packagePath).version + (cliOnly ? ' (cli-only)' : ''); - } catch {} - } - - return version || ''; - } - - private async getPackageManager(): Promise { - try { - const manager = await getPackageManager(this.context.root); - const version = execSync(`${manager} --version`, { encoding: 'utf8', stdio: 'pipe' }).trim(); - - return `${manager} ${version}`; - } catch { - return ''; - } - } -} diff --git a/packages/angular/cli/commands/version.json b/packages/angular/cli/commands/version.json deleted file mode 100644 index 8f4c3ff1fdd1..000000000000 --- a/packages/angular/cli/commands/version.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/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/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index 679757fb6580..ac7591e43630 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -3,42 +3,68 @@ * Copyright Google LLC All Rights 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 { format } from 'util'; -import { runCommand } from '../../models/command-runner'; -import { colors, removeColor } from '../../utilities/color'; -import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config'; -import { writeErrorToLogFile } from '../../utilities/log-file'; -import { findWorkspaceFile } from '../../utilities/project'; +import { logging } from '@angular-devkit/core'; +import { format, stripVTControlCharacters } from 'node:util'; +import { CommandModuleError } from '../../src/command-builder/command-module'; +import { runCommand } from '../../src/command-builder/command-runner'; +import { colors, supportColor } from '../../src/utilities/color'; +import { ngDebug } from '../../src/utilities/environment-options'; +import { writeErrorToLogFile } from '../../src/utilities/log-file'; -export { VERSION, Version } from '../../models/version'; +export { VERSION } from '../../src/utilities/version'; -const debugEnv = process.env['NG_DEBUG']; -const isDebug = debugEnv !== undefined && debugEnv !== '0' && debugEnv.toLowerCase() !== 'false'; +const MIN_NODEJS_VERSION = [20, 19] as const; /* eslint-disable no-console */ -export default async function (options: { testing?: boolean; cliArgs: string[] }) { +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 version = process.versions.node.split('.').map((part) => Number(part)); - if (version[0] < 12 || (version[0] === 12 && version[1] < 14)) { + 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 v12.14.\n\n' + + `The Angular CLI requires a minimum of v${MIN_NODEJS_VERSION[0]}.${MIN_NODEJS_VERSION[1]}.\n\n` + 'Please update your Node.js version or visit https://nodejs.org/ for additional instructions.\n', ); return 3; } - const logger = createConsoleLogger(isDebug, process.stdout, process.stderr, { - info: (s) => (colors.enabled ? s : removeColor(s)), - debug: (s) => (colors.enabled ? s : removeColor(s)), - warn: (s) => (colors.enabled ? colors.bold.yellow(s) : removeColor(s)), - error: (s) => (colors.enabled ? colors.bold.red(s) : removeColor(s)), - fatal: (s) => (colors.enabled ? colors.bold.red(s) : removeColor(s)), + 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; + } + + 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; + } }); // Redirect console to logger @@ -52,39 +78,12 @@ export default async function (options: { testing?: boolean; cliArgs: string[] } logger.error(format(...args)); }; - let workspace; - const workspaceFile = findWorkspaceFile(); - if (workspaceFile === 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.', - ); - - return 1; - } - } else { - try { - workspace = await AngularWorkspace.load(workspaceFile); - } catch (e) { - logger.fatal(`Unable to read workspace file '${workspaceFile}': ${e.message}`); - - return 1; - } - } - try { - const maybeExitCode = await runCommand(options.cliArgs, logger, workspace); - if (typeof maybeExitCode === 'number') { - console.assert(Number.isInteger(maybeExitCode)); - - return maybeExitCode; - } - - return 0; + return await runCommand(options.cliArgs, logger); } catch (err) { - if (err instanceof Error) { + if (err instanceof CommandModuleError) { + logger.fatal(`Error: ${err.message}`); + } else if (err instanceof Error) { try { const logPath = writeErrorToLogFile(err); logger.fatal( @@ -94,7 +93,7 @@ export default async function (options: { testing?: boolean; cliArgs: string[] } } 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); @@ -107,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) { - // eslint-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/workspace-schema.json b/packages/angular/cli/lib/config/workspace-schema.json index cc45e0b879ad..3fede1746559 100644 --- a/packages/angular/cli/lib/config/workspace-schema.json +++ b/packages/angular/cli/lib/config/workspace-schema.json @@ -20,14 +20,10 @@ "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-Z0-9_-]+/)?[a-zA-Z0-9_-]+$": { + "^(?:@[a-zA-Z0-9._-]+/)?[a-zA-Z0-9._-]+$": { "$ref": "#/definitions/project" } }, @@ -40,14 +36,18 @@ "cliOptions": { "type": "object", "properties": { - "defaultCollection": { - "description": "The default schematics collection to use.", - "type": "string" + "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"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "warnings": { "description": "Control CLI specific console warnings", @@ -57,25 +57,77 @@ "description": "Show a warning when the global version is newer than the local one.", "type": "boolean" } - } + }, + "additionalProperties": false }, "analytics": { "type": ["boolean", "string"], - "description": "Share anonymous usage data with the Angular Team at Google." + "description": "Share pseudonymous usage data with the Angular Team at Google." }, - "analyticsSharing": { + "cache": { + "description": "Control disk cache.", "type": "object", "properties": { - "tracking": { - "description": "Analytics sharing info tracking ID.", + "environment": { + "description": "Configure in which environment disk cache is enabled.", "type": "string", - "pattern": "^GA-\\d+-\\d+$" + "enum": ["local", "ci", "all"] + }, + "enabled": { + "description": "Configure whether disk caching is enabled.", + "type": "boolean" }, - "uuid": { - "description": "Analytics sharing info universally unique identifier.", + "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", "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 @@ -126,9 +178,7 @@ "$ref": "../../../../schematics/angular/web-worker/schema.json" } }, - "additionalProperties": { - "type": "object" - } + "additionalProperties": true }, "fileVersion": { "type": "integer", @@ -139,7 +189,14 @@ "type": "object", "properties": { "cli": { - "$ref": "#/definitions/cliOptions" + "schematicCollections": { + "type": "array", + "description": "The list of schematic collections to use.", + "items": { + "type": "string", + "uniqueItems": true + } + } }, "schematics": { "$ref": "#/definitions/schematicOptions" @@ -218,18 +275,42 @@ }, { "type": "object", - "description": "Localization options to use for the source locale", + "description": "Localization options to use for the source locale.", "properties": { "code": { "type": "string", - "description": "Specifies the locale code of the source locale", + "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": "HTML base HREF to use for the locale (defaults to the locale code)" + "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 } ] @@ -242,11 +323,11 @@ "oneOf": [ { "type": "string", - "description": "Localization file to use for i18n" + "description": "Localization file to use for i18n." }, { "type": "array", - "description": "Localization files to use for i18n", + "description": "Localization files to use for i18n.", "items": { "type": "string", "uniqueItems": true @@ -254,17 +335,17 @@ }, { "type": "object", - "description": "Localization options to use for the locale", + "description": "Localization options to use for the locale.", "properties": { "translation": { "oneOf": [ { "type": "string", - "description": "Localization file to use for i18n" + "description": "Localization file to use for i18n." }, { "type": "array", - "description": "Localization files to use for i18n", + "description": "Localization files to use for i18n.", "items": { "type": "string", "uniqueItems": true @@ -274,9 +355,33 @@ }, "baseHref": { "type": "string", - "description": "HTML base HREF to use for the locale (defaults to the locale code)" + "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 } ] @@ -297,15 +402,25 @@ "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:protractor", + "@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:tslint", - "@angular-devkit/build-angular:ng-packagr" + "@angular-devkit/build-angular:ssr-dev-server" ] } }, @@ -327,6 +442,50 @@ "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, @@ -339,12 +498,12 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/app-shell/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/app-shell/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/app-shell/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/app-shell/schema.json" } } } @@ -361,12 +520,56 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/browser/schema.json" + "$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/browser/schema.json" + "$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" } } } @@ -383,12 +586,34 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/dev-server/schema.json" + "$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_devkit/build_angular/src/dev-server/schema.json" + "$ref": "../../../../angular/build/src/builders/extract-i18n/schema.json" } } } @@ -405,12 +630,56 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/extract-i18n/schema.json" + "$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_devkit/build_angular/src/extract-i18n/schema.json" + "$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" } } } @@ -427,12 +696,12 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/karma/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/karma/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/karma/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/karma/schema.json" } } } @@ -442,19 +711,19 @@ "additionalProperties": false, "properties": { "builder": { - "const": "@angular-devkit/build-angular:protractor" + "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/protractor/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/protractor/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json" } } } @@ -464,19 +733,41 @@ "additionalProperties": false, "properties": { "builder": { - "const": "@angular-devkit/build-angular:server" + "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/server/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/prerender/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/server/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/prerender/schema.json" } } } @@ -486,19 +777,41 @@ "additionalProperties": false, "properties": { "builder": { - "const": "@angular-devkit/build-angular:tslint" + "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/tslint/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/ssr-dev-server/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/tslint/schema.json" + "$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" } } } @@ -515,12 +828,34 @@ "description": "A default named configuration to use when a target configuration is not provided." }, "options": { - "$ref": "../../../../angular_devkit/build_angular/src/ng-packagr/schema.json" + "$ref": "../../../../angular_devkit/build_angular/src/builders/ng-packagr/schema.json" }, "configurations": { "type": "object", "additionalProperties": { - "$ref": "../../../../angular_devkit/build_angular/src/ng-packagr/schema.json" + "$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" } } } @@ -533,14 +868,13 @@ "type": "object", "properties": { "$schema": { - "type": "string", - "format": "uri" + "type": "string" }, "version": { "$ref": "#/definitions/fileVersion" }, "cli": { - "$ref": "#/definitions/cliOptions" + "$ref": "#/definitions/cliGlobalOptions" }, "schematics": { "$ref": "#/definitions/schematicOptions" diff --git a/packages/angular/cli/lib/examples/if-block.md b/packages/angular/cli/lib/examples/if-block.md new file mode 100644 index 000000000000..806e3d05516c --- /dev/null +++ b/packages/angular/cli/lib/examples/if-block.md @@ -0,0 +1,85 @@ +--- +title: 'Using the @if Built-in Control Flow Block' +summary: 'Demonstrates how to use the @if built-in control flow block to conditionally render content in an Angular template based on a boolean expression.' +keywords: + - '@if' + - 'control flow' + - 'conditional rendering' + - 'template syntax' +related_concepts: + - '@else' + - '@else if' + - 'signals' +related_tools: + - 'modernize' +--- + +## Purpose + +The purpose of this pattern is to create dynamic user interfaces by controlling which elements are rendered to the DOM based on the application's state. This is a fundamental technique for building responsive and interactive components. + +## When to Use + +Use the `@if` block as the modern, preferred alternative to the `*ngIf` directive for all conditional rendering. It offers better type-checking and a cleaner, more intuitive syntax within the template. + +## Key Concepts + +- **`@if` block:** The primary syntax for conditional rendering in modern Angular templates. It evaluates a boolean expression and renders the content within its block if the expression is true. + +## Example Files + +### `conditional-content.component.ts` + +This is a self-contained standalone component that demonstrates the `@if` block with an optional `@else` block. + +```typescript +import { Component, signal } from '@angular/core'; + +@Component({ + selector: 'app-conditional-content', + template: ` + + + @if (isVisible()) { +
This content is conditionally displayed.
+ } @else { +
The content is hidden. Click the button to show it.
+ } + `, +}) +export class ConditionalContentComponent { + protected readonly isVisible = signal(true); + + toggleVisibility(): void { + this.isVisible.update((v) => !v); + } +} +``` + +## Usage Notes + +- The expression inside the `@if ()` block must evaluate to a boolean. +- This example uses a signal, which is a common pattern, but any boolean property or method call from the component can be used. +- The `@else` block is optional and is rendered when the `@if` condition is `false`. + +## How to Use This Example + +### 1. Import the Component + +In a standalone architecture, import the component into the `imports` array of the parent component where you want to use it. + +```typescript +// in app.component.ts +import { Component } from '@angular/core'; +import { ConditionalContentComponent } from './conditional-content.component'; + +@Component({ + selector: 'app-root', + imports: [ConditionalContentComponent], + template: ` +

My Application

+ + `, +}) +export class AppComponent {} +``` diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 207d0a4ddf97..cd324b6df69b 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -3,54 +3,27 @@ * Copyright Google LLC All Rights 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 -import * as fs from 'fs'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { colors } from '../utilities/color'; -import { isWarningEnabled } from '../utilities/config'; +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'; -// 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) => unknown; - }; - try { - profiler = require('v8-profiler-node8'); // eslint-disable-line import/no-extraneous-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}`, - ); - } - - profiler.startProfiling(); - - const exitHandler = (options: { cleanup?: boolean; exit?: boolean }) => { - if (options.cleanup) { - const cpuProfile = profiler.stopProfiling(); - fs.writeFileSync( - path.resolve(process.cwd(), process.env.NG_CLI_PROFILING || '') + '.cpuprofile', - JSON.stringify(cpuProfile), - ); - } - - if (options.exit) { - process.exit(); - } - }; - - process.on('exit', () => exitHandler({ cleanup: true })); - process.on('SIGINT', () => exitHandler({ exit: true })); - process.on('uncaughtException', () => exitHandler({ exit: true })); -} +/** + * 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 () => { +(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. @@ -58,54 +31,78 @@ if (process.env['NG_CLI_PROFILING']) { * See: https://github.com/browserslist/browserslist/blob/819c4337456996d19db6ba953014579329e9c6e1/node.js#L324 */ process.env.BROWSERSLIST_IGNORE_OLD_DATA = '1'; + const rawCommandName = process.argv[2]; - const disableVersionCheckEnv = process.env['NG_DISABLE_VERSION_CHECK']; /** * 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 */ - const disableVersionCheck = - disableVersionCheckEnv !== undefined && - disableVersionCheckEnv !== '0' && - disableVersionCheckEnv.toLowerCase() !== 'false'; - - if (disableVersionCheck) { + if (disableVersionCheck || rawCommandName === 'new') { return (await import('./cli')).default; } let cli; + try { // No error implies a projectLocalCli, which will load whatever // version of ng-cli you have installed in a local package.json - const projectLocalCli = require.resolve('@angular/cli', { paths: [process.cwd()] }); + const cwdRequire = createRequire(process.cwd() + '/'); + const projectLocalCli = cwdRequire.resolve('@angular/cli'); cli = await import(projectLocalCli); - const globalVersion = new SemVer(require('../package.json').version); + const globalVersion = new SemVer(VERSION.full); // Older versions might not have the VERSION export let localVersion = cli.VERSION?.full; if (!localVersion) { try { - localVersion = require(path.join(path.dirname(projectLocalCli), '../../package.json')) - .version; + 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); } } + // Ensure older versions of the CLI fully exit + const localMajorVersion = major(localVersion); + if (localMajorVersion > 0 && localMajorVersion < 14) { + forceExit = true; + + // Versions prior to 14 didn't implement completion command. + if (rawCommandName === 'completion') { + return null; + } + } + let isGlobalGreater = false; try { - isGlobalGreater = !!localVersion && globalVersion.compare(localVersion) > 0; + 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); } - if (isGlobalGreater) { + // 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 (process.argv[2] === 'update') { + 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 @@ -132,15 +129,16 @@ if (process.env['NG_CLI_PROFILING']) { return cli; })() - .then((cli) => { - return cli({ + .then((cli) => + cli?.({ cliArgs: process.argv.slice(2), - inputStream: process.stdin, - outputStream: process.stdout, - }); - }) - .then((exitCode: number) => { - process.exit(exitCode); + }), + ) + .then((exitCode = 0) => { + if (forceExit) { + process.exit(exitCode); + } + process.exitCode = exitCode; }) .catch((err: Error) => { // eslint-disable-next-line no-console diff --git a/packages/angular/cli/models/analytics-collector.ts b/packages/angular/cli/models/analytics-collector.ts deleted file mode 100644 index da775363f34f..000000000000 --- a/packages/angular/cli/models/analytics-collector.ts +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { execSync } from 'child_process'; -import * as debug from 'debug'; -import * as https from 'https'; -import * as os from 'os'; -import * as querystring from 'querystring'; -import { VERSION } from './version'; - -interface BaseParameters extends analytics.CustomDimensionsAndMetricsOptions { - [key: string]: string | number | boolean | undefined | (string | number | boolean | undefined)[]; -} - -interface ScreenviewParameters extends BaseParameters { - /** Screen Name */ - cd?: string; - /** Application Name */ - an?: string; - /** Application Version */ - av?: string; - /** Application ID */ - aid?: string; - /** Application Installer ID */ - aiid?: string; -} - -interface TimingParameters extends BaseParameters { - /** User timing category */ - utc?: string; - /** User timing variable name */ - utv?: string; - /** User timing time */ - utt?: string | number; - /** User timing label */ - utl?: string; -} - -interface PageviewParameters extends BaseParameters { - /** - * Document Path - * The path portion of the page URL. Should begin with '/'. - */ - dp?: string; - /** Document Host Name */ - dh?: string; - /** Document Title */ - dt?: string; - /** - * Document location URL - * Use this parameter to send the full URL (document location) of the page on which content resides. - */ - dl?: string; -} - -interface EventParameters extends BaseParameters { - /** Event Category */ - ec: string; - /** Event Action */ - ea: string; - /** Event Label */ - el?: string; - /** - * Event Value - * Specifies the event value. Values must be non-negative. - */ - ev?: string | number; - /** Page Path */ - p?: string; - /** Page */ - dp?: string; -} - -/** - * See: https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide - */ -export class AnalyticsCollector implements analytics.Analytics { - private trackingEventsQueue: Record[] = []; - private readonly parameters: Record = {}; - private readonly analyticsLogDebug = debug('ng:analytics:log'); - - constructor(trackingId: string, userId: string) { - // API Version - this.parameters['v'] = '1'; - // User ID - this.parameters['cid'] = userId; - // Tracking - this.parameters['tid'] = trackingId; - - this.parameters['ds'] = 'cli'; - this.parameters['ua'] = _buildUserAgentString(); - this.parameters['ul'] = _getLanguage(); - - // @angular/cli with version. - this.parameters['an'] = '@angular/cli'; - this.parameters['av'] = VERSION.full; - - // We use the application ID for the Node version. This should be "node v12.10.0". - const nodeVersion = `node ${process.version}`; - this.parameters['aid'] = nodeVersion; - - // Custom dimentions - // We set custom metrics for values we care about. - this.parameters['cd' + analytics.NgCliAnalyticsDimensions.CpuCount] = os.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. - this.parameters['cd' + analytics.NgCliAnalyticsDimensions.CpuSpeed] = Math.floor( - os.cpus()[0].speed, - ); - this.parameters['cd' + analytics.NgCliAnalyticsDimensions.RamInGigabytes] = Math.round( - os.totalmem() / (1024 * 1024 * 1024), - ); - this.parameters['cd' + analytics.NgCliAnalyticsDimensions.NodeVersion] = nodeVersion; - } - - event(ec: string, ea: string, options: analytics.EventOptions = {}): void { - const { label: el, value: ev, metrics, dimensions } = options; - this.addToQueue('event', { ec, ea, el, ev, metrics, dimensions }); - } - - pageview(dp: string, options: analytics.PageviewOptions = {}): void { - const { hostname: dh, title: dt, metrics, dimensions } = options; - this.addToQueue('pageview', { dp, dh, dt, metrics, dimensions }); - } - - timing( - utc: string, - utv: string, - utt: string | number, - options: analytics.TimingOptions = {}, - ): void { - const { label: utl, metrics, dimensions } = options; - this.addToQueue('timing', { utc, utv, utt, utl, metrics, dimensions }); - } - - screenview(cd: string, an: string, options: analytics.ScreenviewOptions = {}): void { - const { appVersion: av, appId: aid, appInstallerId: aiid, metrics, dimensions } = options; - this.addToQueue('screenview', { cd, an, av, aid, aiid, metrics, dimensions }); - } - - async flush(): Promise { - const pending = this.trackingEventsQueue.length; - this.analyticsLogDebug(`flush queue size: ${pending}`); - - if (!pending) { - return; - } - - // The below is needed so that if flush is called multiple times, - // we don't report the same event multiple times. - const pendingTrackingEvents = this.trackingEventsQueue; - this.trackingEventsQueue = []; - - try { - await this.send(pendingTrackingEvents); - } catch (error) { - // Failure to report analytics shouldn't crash the CLI. - this.analyticsLogDebug('send error: %j', error); - } - } - - private addToQueue(eventType: 'event', parameters: EventParameters): void; - private addToQueue(eventType: 'pageview', parameters: PageviewParameters): void; - private addToQueue(eventType: 'timing', parameters: TimingParameters): void; - private addToQueue(eventType: 'screenview', parameters: ScreenviewParameters): void; - private addToQueue( - eventType: 'event' | 'pageview' | 'timing' | 'screenview', - parameters: BaseParameters, - ): void { - const { metrics, dimensions, ...restParameters } = parameters; - const data = { - ...this.parameters, - ...restParameters, - ...this.customVariables({ metrics, dimensions }), - t: eventType, - }; - - this.analyticsLogDebug('add event to queue: %j', data); - this.trackingEventsQueue.push(data); - } - - private async send(data: Record[]): Promise { - this.analyticsLogDebug('send event: %j', data); - - return new Promise((resolve, reject) => { - const request = https.request( - { - host: 'www.google-analytics.com', - method: 'POST', - path: data.length > 1 ? '/batch' : '/collect', - }, - (response) => { - if (response.statusCode !== 200) { - reject( - new Error(`Analytics reporting failed with status code: ${response.statusCode}.`), - ); - - return; - } - }, - ); - - request.on('error', reject); - - const queryParameters = data.map((p) => querystring.stringify(p)).join('\n'); - request.write(queryParameters); - request.end(resolve); - }); - } - - /** - * Creates the dimension and metrics variables to add to the queue. - * @private - */ - private customVariables( - options: analytics.CustomDimensionsAndMetricsOptions, - ): Record { - const additionals: Record = {}; - - const { dimensions, metrics } = options; - dimensions?.forEach((v, i) => (additionals[`cd${i}`] = v)); - metrics?.forEach((v, i) => (additionals[`cm${i}`] = v)); - - return additionals; - } -} - -// 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: Readonly<{ [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. This gets sent to Analytics so it shows the proper OS version. - * @private - */ -function _buildUserAgentString() { - switch (os.platform()) { - case 'darwin': { - 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()})`; - } - - case 'win32': - return `(Windows NT ${os.release()})`; - - case 'linux': - return `(X11; Linux i686; ${os.release()}; ${os.cpus()[0].model})`; - - default: - return os.platform() + ' ' + os.release(); - } -} - -/** - * 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() || - '??' - ); // ¯\_(ツ)_/¯ -} - -/** - * 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 execSync('wmic.exe os get locale').toString().trim(); - } catch {} - - return undefined; -} diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts deleted file mode 100644 index e3ab53593f6f..000000000000 --- a/packages/angular/cli/models/analytics.ts +++ /dev/null @@ -1,369 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { json, tags } from '@angular-devkit/core'; -import * as debug from 'debug'; -import * as inquirer from 'inquirer'; -import { v4 as uuidV4 } from 'uuid'; -import { colors } from '../utilities/color'; -import { getWorkspace, getWorkspaceRaw } from '../utilities/config'; -import { isTTY } from '../utilities/tty'; -import { AnalyticsCollector } from './analytics-collector'; - -/* eslint-disable no-console */ -const analyticsDebug = debug('ng:analytics'); // Generate analytics, including settings and users. - -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', -]; - -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 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 cli = config.get(['cli']); - - if (cli !== undefined && !json.isJsonObject(cli as json.JsonValue)) { - throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`); - } - - if (value === true) { - value = uuidV4(); - } - - config.modify(['cli', 'analytics'], value); - config.save(); - - 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 https://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(''); - - // Send back a ping with the user `optin`. - const ua = new AnalyticsCollector(AnalyticsProperties.AngularCliDefault, 'optin'); - ua.pageview('/telemetry/optin'); - await ua.flush(); - } else { - // Send back a ping with the user `optout`. This is the only thing we send. - const ua = new AnalyticsCollector(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 https://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(''); - - // Send back a ping with the user `optin`. - const ua = new AnalyticsCollector(AnalyticsProperties.AngularCliDefault, 'optin'); - ua.pageview('/telemetry/project/optin'); - await ua.flush(); - } else { - // Send back a ping with the user `optout`. This is the only thing we send. - const ua = new AnalyticsCollector(AnalyticsProperties.AngularCliDefault, 'optout'); - ua.pageview('/telemetry/project/optout'); - await ua.flush(); - } - - return true; - } - - return false; -} - -export async function hasGlobalAnalyticsConfiguration(): Promise { - try { - const globalWorkspace = await 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 async function getGlobalAnalytics(): Promise { - 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 AnalyticsCollector(propertyId, 'ci'); - } - } - - // If anything happens we just keep the NOOP analytics. - try { - const globalWorkspace = await 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 AnalyticsCollector(propertyId, uid); - } - } catch (err) { - analyticsDebug('Error happened during reading of analytics config: %s', err.message); - - return undefined; - } -} - -export async function hasWorkspaceAnalyticsConfiguration(): Promise { - try { - const globalWorkspace = await getWorkspace('local'); - const analyticsConfig: string | undefined | null | { uid?: string } = - globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics']; - - if (analyticsConfig !== undefined) { - return true; - } - } catch {} - - return false; -} - -/** - * Get the workspace analytics object for the user. This returns an instance of AnalyticsCollector, - * or undefined if analytics are disabled. - * - * If any problem happens, it is considered the user has been opting out of analytics. - */ -export async function getWorkspaceAnalytics(): Promise { - analyticsDebug('getWorkspaceAnalytics'); - try { - const globalWorkspace = await getWorkspace('local'); - const analyticsConfig: string | undefined | null | { uid?: string } = globalWorkspace?.getCli()[ - 'analytics' - ]; - analyticsDebug('Workspace 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.'); - - 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 AnalyticsCollector(AnalyticsProperties.AngularCliDefault, 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 async function getSharedAnalytics(): Promise { - 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 = await getWorkspace('global'); - const analyticsConfig = globalWorkspace?.getCli()['analyticsSharing']; - - if (!analyticsConfig || !analyticsConfig.tracking || !analyticsConfig.uuid) { - return undefined; - } else { - analyticsDebug('Analytics sharing info: %j', analyticsConfig); - - return new AnalyticsCollector(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 d98ddbabe340..000000000000 --- a/packages/angular/cli/models/architect-command.ts +++ /dev/null @@ -1,381 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { json, schema, tags } from '@angular-devkit/core'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { isPackageNameSafeForAnalytics } from './analytics'; -import { BaseCommandOptions, Command } from './command'; -import { Arguments, Option } from './interface'; -import { parseArguments } from './parser'; - -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 _registry!: json.schema.SchemaRegistry; - protected readonly useReportAnalytics = false; - - // If this command supports running multiple targets. - protected multiTarget = false; - - target: string | undefined; - missingTargetError: string | undefined; - - public async initialize(options: T & Arguments): Promise { - this._registry = new json.schema.CoreSchemaRegistry(); - this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - this._registry.useXDeprecatedProvider((msg) => this.logger.warn(msg)); - - if (!this.workspace) { - this.logger.fatal('A workspace is required for this command.'); - - return 1; - } - - this._architectHost = new WorkspaceNodeModulesArchitectHost( - this.workspace, - this.workspace.basePath, - ); - 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) { - this.logger.fatal('Cannot determine project or target for command.'); - - return 1; - } - - return; - } - - let projectName = options.project; - if (projectName && !this.workspace.projects.has(projectName)) { - this.logger.fatal(`Project '${projectName}' does not exist.`); - - return 1; - } - - const commandLeftovers = options['--']; - const targetProjectNames: string[] = []; - for (const [name, project] of this.workspace.projects) { - if (project.targets.has(this.target)) { - targetProjectNames.push(name); - } - } - - if (targetProjectNames.length === 0) { - this.logger.fatal( - this.missingTargetError || `No projects support the '${this.target}' target.`, - ); - - return 1; - } - - if (projectName && !targetProjectNames.includes(projectName)) { - this.logger.fatal( - this.missingTargetError || - `Project '${projectName}' does not support the '${this.target}' target.`, - ); - - return 1; - } - - 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) { - this.logger.fatal(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 ')} - `); - - return 1; - } - } - - if (!projectName && !this.multiTarget) { - const defaultProjectName = this.workspace.extensions['defaultProject'] as string; - 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 { - this.logger.fatal( - this.missingTargetError || 'Cannot determine project or target for command.', - ); - - return 1; - } - } - - 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 && !isPackageNameSafeForAnalytics(builderConf)) { - o.userAnalytics = undefined; - } - } - } - - async run(options: ArchitectCommandOptions & Arguments) { - return await this.runArchitectTarget(options); - } - - protected async runSingleTarget(target: Target, targetOptions: string[]) { - // 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; - } - - await this.reportAnalytics([this.description.name], { - ...((await this._architectHost.getOptionsForTarget(target)) as unknown as T), - ...overrides, - }); - - 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); - } - - return result; - } else { - return await this.runSingleTarget(targetSpec, extra); - } - } 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: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const [name, project] of this.workspace!.projects) { - if (project.targets.has(targetName)) { - allProjectsForTargetName.push(name); - } - } - - 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. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maybeDefaultProject = this.workspace!.extensions['defaultProject'] as string; - 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; - if (commandOptions.prod) { - const defaultConfig = - project && - target && - this.workspace?.projects.get(project)?.targets.get(target)?.defaultConfiguration; - - this.logger.warn( - defaultConfig === 'production' - ? 'Option "--prod" is deprecated: No need to use this option as this builder defaults to configuration "production".' - : 'Option "--prod" is deprecated: Use "--configuration production" instead.', - ); - // The --prod flag will always be the first configuration, available to be overwritten - // by following configurations. - configuration = 'production'; - } - if (commandOptions.configuration) { - configuration = `${configuration ? `${configuration},` : ''}${ - commandOptions.configuration - }`; - } - } - - 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 0b8b01fe4baa..000000000000 --- a/packages/angular/cli/models/command-runner.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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, - isJsonObject, - json, - logging, - schema, - strings, - tags, -} from '@angular-devkit/core'; -import { readFileSync } from 'fs'; -import { join, resolve } from 'path'; -import { AngularWorkspace } from '../utilities/config'; -import { readAndParseJson } from '../utilities/json-file'; -import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; -import { - getGlobalAnalytics, - getSharedAnalytics, - getWorkspaceAnalytics, - hasWorkspaceAnalyticsConfiguration, - promptProjectAnalytics, -} from './analytics'; -import { Command } from './command'; -import { CommandDescription } from './interface'; -import * as parser from './parser'; - -// 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', - 'deploy': '../commands/deploy.json', - 'config': '../commands/config.json', - 'doc': '../commands/doc.json', - 'e2e': '../commands/e2e.json', - 'extract-i18n': '../commands/extract-i18n.json', - 'make-this-awesome': '../commands/easter-egg.json', - 'generate': '../commands/generate.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', -}; - -export interface CommandMapOptions { - [key: string]: string; -} - -/** - * Create the analytics instance. - * @private - */ -async function _createAnalytics( - workspace: boolean, - skipPrompt = false, -): Promise { - let config = await getGlobalAnalytics(); - // If in workspace and global analytics is enabled, defer to workspace level - if (workspace && config) { - const skipAnalytics = - skipPrompt || - (process.env['NG_CLI_ANALYTICS'] && - (process.env['NG_CLI_ANALYTICS'].toLowerCase() === 'false' || - process.env['NG_CLI_ANALYTICS'] === '0')); - // 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. - if (!skipAnalytics && !(await hasWorkspaceAnalyticsConfiguration())) { - await promptProjectAnalytics(); - } - config = await getWorkspaceAnalytics(); - } - - const maybeSharedAnalytics = await 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 schema = readAndParseJson(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: AngularWorkspace | undefined, - commands: CommandMapOptions = standardCommands, - options: { analytics?: analytics.Analytics; currentDirectory: string } = { - currentDirectory: process.cwd(), - }, -): 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(!!workspace, description.name === 'update')); - const context = { - workspace, - analytics, - currentDirectory: options.currentDirectory, - root: workspace?.basePath ?? options.currentDirectory, - }; - const command = new description.impl(context, description, logger); - - // Flush on an interval (if the event loop is waiting). - let analyticsFlushPromise = Promise.resolve(); - const analyticsFlushInterval = setInterval(() => { - analyticsFlushPromise = analyticsFlushPromise.then(() => analytics.flush()); - }, 1000); - - const result = await command.validateAndRun(parsedOptions); - - // Flush one last time. - clearInterval(analyticsFlushInterval); - 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 a985cac86528..000000000000 --- a/packages/angular/cli/models/command.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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, logging, strings, tags } from '@angular-devkit/core'; -import { colors } from '../utilities/color'; -import { AngularWorkspace } from '../utilities/config'; -import { - Arguments, - CommandContext, - CommandDescription, - CommandDescriptionMap, - CommandScope, - Option, - SubCommandDescription, -} from './interface'; - -export interface BaseCommandOptions { - help?: boolean | string; -} - -export abstract class Command { - protected allowMissingWorkspace = false; - protected useReportAnalytics = true; - readonly workspace?: AngularWorkspace; - readonly analytics: analytics.Analytics; - - protected static commandMap: () => Promise; - static setCommandMap(map: () => Promise) { - this.commandMap = map; - } - - constructor( - protected readonly 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 {} - - async printHelp(): Promise { - await this.printHelpUsage(); - await this.printHelpOptions(); - - return 0; - } - - async printJsonHelp(): Promise { - const replacer = (key: string, value: string) => - key === 'name' ? strings.dasherize(value) : value; - this.logger.info(JSON.stringify(this.description, replacer, 2)); - - 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 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) { - 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 "${this.workspace.filePath}". - `); - // eslint-disable-next-line no-throw-literal - throw 1; - } - break; - case CommandScope.InProject: - if (!this.workspace) { - 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. - `); - // eslint-disable-next-line no-throw-literal - throw 1; - } - break; - case CommandScope.Everywhere: - // Can't miss this. - break; - } - } - - async reportAnalytics( - paths: string[], - options: 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 run(options: T & Arguments): Promise; - - async validateAndRun(options: T & Arguments): Promise { - if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { - await this.validateScope(); - } - let result = await this.initialize(options); - if (typeof result === 'number' && result !== 0) { - return result; - } - - if (options.help === true) { - return this.printHelp(); - } else if (options.help === 'json' || options.help === 'JSON') { - return this.printJsonHelp(); - } else { - const startTime = +new Date(); - if (this.useReportAnalytics) { - await this.reportAnalytics([this.description.name], options); - } - 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 dacdd2f3a38d..000000000000 --- a/packages/angular/cli/models/error.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 9c908d913247..000000000000 --- a/packages/angular/cli/models/interface.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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'; -import { AngularWorkspace } from '../utilities/config'; - -/** - * 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 command runner context. - */ -export interface CommandContext { - currentDirectory: string; - root: string; - - workspace?: AngularWorkspace; - - // This property 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; - - /** - * 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; - - /** - * 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; -} - -/** - * 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 b1e98d0b3f2a..000000000000 --- a/packages/angular/cli/models/parser.ts +++ /dev/null @@ -1,405 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 && (!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('-') && _coerce(undefined, maybeOption) !== undefined) { - // Verify if not having a value results in a correct parse, if so don't shift. - 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 && option.type !== OptionType.Array) { - 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; - } - } 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); - } - - if (/^[a-z]+[A-Z]/.test(key)) { - warnings.push( - 'Support for camel case arguments has been deprecated and will be removed in a future major version.\n' + - `Use '--${strings.dasherize(key)}' instead of '--${key}'.`, - ); - } - } - - 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); - } - } - - if (leftovers.length) { - 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 1f543d8d560e..000000000000 --- a/packages/angular/cli/models/parser_spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { 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 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 af20499397d1..000000000000 --- a/packages/angular/cli/models/schematic-command.ts +++ /dev/null @@ -1,599 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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, normalize, schema, strings, tags, workspaces } from '@angular-devkit/core'; -import { - DryRunEvent, - UnsuccessfulWorkflowExecution, - formats, - workflow, -} from '@angular-devkit/schematics'; -import { - FileSystemCollection, - FileSystemEngine, - FileSystemSchematic, - NodeWorkflow, -} from '@angular-devkit/schematics/tools'; -import * as inquirer from 'inquirer'; -import * as systemPath from 'path'; -import { colors } from '../utilities/color'; -import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../utilities/config'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { ensureCompatibleNpm, 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'; -import { SchematicEngineHost } from './schematic-engine-host'; - -export interface BaseSchematicSchema { - debug?: boolean; - dryRun?: boolean; - force?: boolean; - interactive?: boolean; - defaults?: boolean; - packageRegistry?: string; -} - -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 { - protected readonly allowPrivateSchematics: boolean = false; - protected readonly useReportAnalytics = false; - protected _workflow!: NodeWorkflow; - - protected 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.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.description = schematic.description.description; - 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 && !isPackageNameSafeForAnalytics(this.collectionName)) { - o.userAnalytics = undefined; - } - } - } - } - - public async printHelp() { - await super.printHelp(); - 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 = await 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}`); - }); - }); - } - - 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 == (await 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 async createWorkflow(options: BaseSchematicSchema): Promise { - if (this._workflow) { - return this._workflow; - } - - const { force, dryRun } = options; - const root = this.context.root; - const workflow = new NodeWorkflow(root, { - force, - dryRun, - packageManager: await getPackageManager(root), - packageRegistry: options.packageRegistry, - // A schema registry is required to allow customizing addUndefinedDefaults - registry: new schema.CoreSchemaRegistry(formats.standardFormats), - resolvePaths: this.workspace - ? // Workspace - this.collectionName === this.defaultCollectionName - ? // Favor __dirname for @schematics/angular to use the build-in version - [__dirname, process.cwd(), root] - : [process.cwd(), root, __dirname] - : // Global - [__dirname, process.cwd()], - schemaValidation: true, - optionTransforms: [ - // Add configuration file defaults - async (schematic, current) => { - const projectName = - typeof (current as Record).project === 'string' - ? ((current as Record).project as string) - : getProjectName(); - - return { - ...(await getSchematicDefaults(schematic.collection.name, schematic.name, projectName)), - ...current, - }; - }, - ], - engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), - }); - - const getProjectName = () => { - if (this.workspace) { - const projectNames = getProjectsByPath( - this.workspace, - process.cwd(), - this.workspace.basePath, - ); - - if (projectNames.length === 1) { - return projectNames[0]; - } else { - if (projectNames.length > 1) { - 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. - `); - } - - const defaultProjectName = this.workspace.extensions['defaultProject']; - if (typeof defaultProjectName === 'string' && defaultProjectName) { - return defaultProjectName; - } - } - } - - return undefined; - }; - - workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); - workflow.registry.addSmartDefaultProvider('projectName', getProjectName); - workflow.registry.useXDeprecatedProvider((msg) => this.logger.warn(msg)); - - let shouldReportAnalytics = true; - workflow.engineHost.registerOptionsTransform(async (_, options) => { - if (shouldReportAnalytics) { - shouldReportAnalytics = false; - await this.reportAnalytics([this.description.name], options as Arguments); - } - - return options; - }); - - if (options.interactive !== false && isTTY()) { - workflow.registry.usePromptProvider((definitions: Array) => { - const questions: inquirer.QuestionCollection = definitions - .filter((definition) => !options.defaults || definition.default === undefined) - .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); - - // Filter allows transformation of the value prior to validation - question.filter = async (input) => { - for (const type of definition.propertyTypes) { - let value; - switch (type) { - case 'string': - value = String(input); - break; - case 'integer': - case 'number': - value = Number(input); - break; - default: - value = input; - break; - } - // Can be a string if validation fails - const isValid = (await validator(value)) === true; - if (isValid) { - return value; - } - } - - return input; - }; - } - - switch (definition.type) { - case 'confirmation': - question.type = 'confirm'; - break; - case 'list': - question.type = definition.multiselect ? 'checkbox' : 'list'; - (question as inquirer.CheckboxQuestion).choices = definition.items?.map((item) => { - return typeof item == 'string' - ? item - : { - name: item.label, - value: item.value, - }; - }); - break; - default: - question.type = definition.type; - break; - } - - return question; - }); - - return inquirer.prompt(questions); - }); - } - - return (this._workflow = workflow); - } - - protected async getDefaultSchematicCollection(): Promise { - let workspace = await 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 = await 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.context.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; - - // 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); - } - - const allowAdditionalProperties = - typeof schematic.description.schemaJson === 'object' && - schematic.description.schemaJson.additionalProperties; - - if (args['--'] && !allowAdditionalProperties) { - args['--'].forEach((additional) => { - this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); - }); - - return 1; - } - - const pathOptions = o ? this.setPathOptions(o, workingDir) : {}; - const input = { - ...pathOptions, - ...args, - ...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.cyan('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': - const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to; - loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`); - 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; - } - }); - - // Temporary compatibility check for NPM 7 - if (collectionName === '@schematics/angular' && schematicName === 'ng-new') { - if ( - !input.skipInstall && - (input.packageManager === undefined || input.packageManager === 'npm') - ) { - await ensureCompatibleNpm(this.context.root); - } - } - - 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 occurred:\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); - } -} - -function getProjectsByPath( - workspace: workspaces.WorkspaceDefinition, - path: string, - root: string, -): string[] { - if (workspace.projects.size === 1) { - return Array.from(workspace.projects.keys()); - } - - const isInside = (base: string, potential: string): boolean => { - const absoluteBase = systemPath.resolve(root, base); - const absolutePotential = systemPath.resolve(root, potential); - const relativePotential = systemPath.relative(absoluteBase, absolutePotential); - if (!relativePotential.startsWith('..') && !systemPath.isAbsolute(relativePotential)) { - return true; - } - - return false; - }; - - const projects = Array.from(workspace.projects.entries()) - .map(([name, project]) => [systemPath.resolve(root, project.root), name] as [string, string]) - .filter((tuple) => isInside(tuple[0], path)) - // 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 === 1) { - return [projects[0][1]]; - } else if (projects.length > 1) { - const firstPath = projects[0][0]; - - return projects.filter((v) => v[0] === firstPath).map((v) => v[1]); - } - - return []; -} diff --git a/packages/angular/cli/models/schematic-engine-host.ts b/packages/angular/cli/models/schematic-engine-host.ts deleted file mode 100644 index 2dca2f3705f2..000000000000 --- a/packages/angular/cli/models/schematic-engine-host.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { RuleFactory, SchematicsException, Tree } from '@angular-devkit/schematics'; -import { NodeModulesEngineHost } from '@angular-devkit/schematics/tools'; -import { readFileSync } from 'fs'; -import { parse as parseJson } from 'jsonc-parser'; -import { dirname, resolve } from 'path'; -import { Script } from 'vm'; - -/** - * Environment variable to control schematic package redirection - * Default: Angular schematics only - */ -const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase(); - -function shouldWrapSchematic(schematicFile: string): boolean { - // Check environment variable if present - if (schematicRedirectVariable !== undefined) { - 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; - } - - // Default is only first-party Angular schematic packages - // Angular schematics are safe to use in the wrapped VM context - return /\/node_modules\/@(?:angular|schematics|nguniversal)\//.test(normalizedSchematicFile); -} - -export class SchematicEngineHost extends NodeModulesEngineHost { - protected _resolveReferenceString(refString: string, parentPath: string) { - 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 schematicFile = require.resolve(fullPath, { paths: [parentPath] }); - - if (shouldWrapSchematic(schematicFile)) { - 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); - } -} - -/** - * 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 { createRequire } = require('module'); - const scopedRequire = 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('@angular-devkit/') || id.startsWith('@schematics/')) { - // Resolve from inside the `@angular/cli` project - const packagePath = require.resolve(id); - - return require(packagePath); - } 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 = scopedRequire.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 scopedRequire(id); - }; - - // Setup a wrapper function to capture the module's exports - const schematicCode = readFileSync(schematicFile, 'utf8'); - // `module` is required due to @angular/localize ng-add being in UMD format - const headerCode = '(function() {\nvar exports = {};\nvar module = { exports };\n'; - const footerCode = exportName ? `\nreturn exports['${exportName}'];});` : '\nreturn exports;});'; - - const script = new Script(headerCode + schematicCode + footerCode, { - filename: schematicFile, - lineOffset: 3, - }); - - const context = { - __dirname: schematicDirectory, - __filename: schematicFile, - Buffer, - console, - process, - get global() { - return this; - }, - require: customRequire, - }; - - const exportsFactory = script.runInNewContext(context); - - return exportsFactory; -} diff --git a/packages/angular/cli/models/version.ts b/packages/angular/cli/models/version.ts deleted file mode 100644 index 0ee4e0c8c828..000000000000 --- a/packages/angular/cli/models/version.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 - */ - -// Same structure as used in framework packages -export class Version { - public readonly major: string; - public readonly minor: string; - public readonly patch: string; - - constructor(public readonly full: string) { - this.major = full.split('.')[0]; - this.minor = full.split('.')[1]; - this.patch = full.split('.').slice(2).join('.'); - } -} - -export const VERSION = new Version(require('../package.json').version); diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 899c2a50c636..1fb8b671f261 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,38 +22,37 @@ }, "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", + "@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.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.25.0", + "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.1", - "debug": "4.3.1", - "ini": "2.0.0", - "inquirer": "8.1.0", - "jsonc-parser": "3.0.0", - "npm-package-arg": "8.1.4", - "npm-pick-manifest": "6.1.1", - "open": "8.2.0", - "ora": "5.4.1", - "pacote": "11.3.4", - "resolve": "1.20.0", - "semver": "7.3.5", - "symbol-observable": "4.0.0", - "uuid": "8.3.2" - }, - "devDependencies": { - "rxjs": "6.6.7" + "algoliasearch": "5.46.0", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.0.4", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", + "yargs": "18.0.0", + "zod": "4.2.1" }, "ng-update": { "migrations": "@schematics/angular/migrations/migration-collection.json", "packageGroup": { - "@angular/cli": "0.0.0", - "@angular-devkit/architect": "0.0.0", - "@angular-devkit/build-angular": "0.0.0", - "@angular-devkit/build-webpack": "0.0.0", - "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "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/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..fb3508777d74 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-base-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 { Architect, Target } from '@angular-devkit/architect'; +import { + NodeModulesBuilderInfo, + WorkspaceNodeModulesArchitectHost, +} from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { createRequire } from 'node:module'; +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; + } + + const workspaceResolve = createRequire(basePath + '/').resolve; + + try { + workspaceResolve('@angular/core'); + + return; + } catch {} + + 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..98e270cf1dad --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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) { + const targetDefinition = projectDefinition.targets.get(target); + if (targetDefinition?.builder) { + continue; + } + + const defaultBuilder = await this.findDefaultBuilderName(projectDefinition, { + project, + target, + }); + if (!defaultBuilder) { + continue; + } + + if (targetDefinition) { + targetDefinition.builder = defaultBuilder; + } else { + 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 originalProcessTitle = process.title; + try { + const target = this.getArchitectTarget(); + const { configuration = '', project, ...architectOptions } = options; + + if (project) { + process.title = `${originalProcessTitle} (${project})`; + + return await this.runSingleTarget({ configuration, target, project }, architectOptions); + } + + // 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) { + process.title = `${originalProcessTitle} (${project})`; + result |= await this.runSingleTarget({ configuration, target, project }, architectOptions); + } + + return result; + } finally { + process.title = originalProcessTitle; + } + } + + 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..d036656cf2dd --- /dev/null +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -0,0 +1,305 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 type { + 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; + yargsInstance: Argv<{}>; + + /** 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< + string, + EventCustomDimension | EventCustomMetric + >(); + + 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) { + if (!validEventCustomDimensionAndMetrics.has(ua)) { + continue; + } + + const value = options[name]; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + parameters[ua] = value; + } else if (Array.isArray(value)) { + // GA doesn't allow array as values. + parameters[ua] = value.sort().join(', '); + } + } + + return parameters; + } + + private reportCommandRunAnalytics(analytics: AnalyticsCollector): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const internalMethods = (this.context.yargsInstance 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..cb4ab2c8467e --- /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 as yargsParser } 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 { createNormalizeOptionsMiddleware } from './utilities/normalize-options-middleware'; + +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 localYargs = yargs(args); + + const context: CommandContext = { + globalConfiguration, + workspace, + logger, + currentDirectory: process.cwd(), + yargsInstance: localYargs, + root, + packageManager: new PackageManagerUtils({ globalConfiguration, workspace, root }), + args: { + positional: positional.map((v) => v.toString()), + options: { + help, + jsonHelp, + getYargsCompletions, + ...rest, + }, + }, + }; + + for (const CommandModule of await getCommandsToRegister(positional[0])) { + addCommandModuleToYargs(CommandModule, context); + } + + if (jsonHelp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usageInstance = (localYargs as any).getInternalMethods().getUsageInstance(); + usageInstance.help = () => jsonHelpUsage(localYargs); + } + + // 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(createNormalizeOptionsMiddleware(localYargs)) + .version(false) + .showHelpOnFail(false) + .strict() + .fail((msg, err) => { + throw msg + ? // Validation failed example: `Unknown argument:` + new CommandModuleError(msg) + : // Unknown exception, re-throw. + err; + }) + .wrap(localYargs.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..ef317700d1a6 --- /dev/null +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -0,0 +1,413 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, + checked: + definition.multiselect && Array.isArray(definition.default) + ? definition.default?.includes(item) + : item === definition.default, + } + : { + ...item, + name: item.label, + value: item.value, + checked: + definition.multiselect && Array.isArray(definition.default) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + definition.default?.includes(item.value as any) + : item.value === definition.default, + }, + ), + }); + 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..8b019aba9064 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/command.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 { 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( + commandModule: U, + context: CommandContext, +): void { + const cmd = new commandModule(context); + const { + args: { + options: { jsonHelp }, + }, + workspace, + } = context; + + const describe = jsonHelp ? cmd.fullDescribe : cmd.describe; + + context.yargsInstance.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..0d5c6a53a1e6 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-help.ts @@ -0,0 +1,156 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { 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(localYargs: Argv): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const localYargsInstance = localYargs as any; + const { + deprecatedOptions, + alias: aliases, + array, + string, + boolean, + number, + choices, + demandedOptions, + default: defaultVal, + hiddenOptions = [], + } = localYargsInstance.getOptions(); + + const internalMethods = localYargsInstance.getInternalMethods(); + const usageInstance = internalMethods.getUsageInstance(); + const context = internalMethods.getContext(); + const descriptions = usageInstance.getDescriptions(); + const groups = localYargsInstance.getGroups(); + const positional = groups[usageInstance.getPositionalGroupName()] as string[] | undefined; + const seen = new Set(); + const hidden = new Set(hiddenOptions); + const normalizeOptions: JsonHelpOption[] = []; + const allAliases = new Set([...Object.values(aliases).flat()]); + + // Reverted order of https://github.com/yargs/yargs/blob/971e351705f0fbc5566c6ed1dfd707fa65e11c0d/lib/usage.ts#L419-L424 + for (const [names, type] of [ + [number, 'number'], + [array, 'array'], + [string, 'string'], + [boolean, 'boolean'], + ]) { + for (const name of names) { + if (allAliases.has(name) || hidden.has(name) || seen.has(name)) { + // Ignore hidden, aliases and already visited option. + continue; + } + + seen.add(name); + 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, localYargsInstance['$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..0a4215be8eed --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-schema.ts @@ -0,0 +1,493 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, strings } from '@angular-devkit/core'; +import type { Arguments, Argv, PositionalOptions, Options as YargsOptions } from 'yargs'; +import { EventCustomDimension } from '../../analytics/analytics-parameters'; + +/** + * An option description that can be used by yargs to create a command. + * See: https://github.com/yargs/yargs/blob/main/docs/options.mjs + */ +export interface Option extends YargsOptions { + /** + * 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'; +} + +/** + * A Yargs check function that validates that the given options are in the form of `key=value`. + * @param keyValuePairOptions A set of options that should be in the form of `key=value`. + * @param args The parsed arguments. + * @returns `true` if the options are valid, otherwise an error is thrown. + */ +function checkStringMap(keyValuePairOptions: Set, args: Arguments): boolean { + for (const key of keyValuePairOptions) { + const value = args[key]; + if (!Array.isArray(value)) { + // Value has been parsed. + continue; + } + + for (const pair of value) { + if (pair === undefined) { + continue; + } + + if (!pair.includes('=')) { + throw new Error( + `Invalid value for argument: ${key}, Given: '${pair}', Expected key=value pair`, + ); + } + } + } + + return true; +} + +/** + * A Yargs coerce function that converts an array of `key=value` strings to an object. + * @param value An array of `key=value` strings. + * @returns An object with the keys and values from the input array. + */ +function coerceToStringMap( + value: (string | undefined)[], +): Record | (string | undefined)[] { + 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) { + // In the case it is not valid skip processing this option and handle the error in `checkStringMap` + return value; + } + + const key = pair.slice(0, eqIdx); + stringMap[key] = pair.slice(eqIdx + 1); + } + + return stringMap; +} + +/** + * Checks if a JSON schema node represents a string map. + * A string map is an object with `additionalProperties` of type `string`. + * @param node The JSON schema node to check. + * @returns `true` if the node represents a string map, otherwise `false`. + */ +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' + ); +} + +const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string']); + +/** + * Checks if a string is a supported primitive type. + * @param value The string to check. + * @returns `true` if the string is a supported primitive type, otherwise `false`. + */ +function isSupportedPrimitiveType(value: string): boolean { + return SUPPORTED_PRIMITIVE_TYPES.has(value); +} + +/** + * Recursively checks if a JSON schema for an array's items is a supported primitive type. + * It supports `oneOf` and `anyOf` keywords. + * @param schema The JSON schema for the array's items. + * @returns `true` if the schema is a supported primitive type, otherwise `false`. + */ +function isSupportedArrayItemSchema(schema: json.JsonObject): boolean { + if (typeof schema.type === 'string' && isSupportedPrimitiveType(schema.type)) { + return true; + } + + if (json.isJsonArray(schema.enum)) { + return true; + } + + if (json.isJsonArray(schema.items)) { + return schema.items.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)); + } + + if ( + json.isJsonArray(schema.oneOf) && + schema.oneOf.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)) + ) { + return true; + } + + if ( + json.isJsonArray(schema.anyOf) && + schema.anyOf.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item)) + ) { + return true; + } + + return false; +} + +/** + * Gets the supported types for a JSON schema node. + * @param current The JSON schema node to get the supported types for. + * @returns An array of supported types. + */ +function getSupportedTypes( + current: json.JsonObject, +): ReadonlyArray<'string' | 'number' | 'boolean' | 'array' | 'object'> { + const typeSet = json.schema.getTypesOfSchema(current); + + if (typeSet.size === 0) { + return []; + } + + return [...typeSet].filter((type) => { + switch (type) { + case 'boolean': + case 'number': + case 'string': + return true; + case 'array': + return isJsonObject(current.items) && isSupportedArrayItemSchema(current.items); + case 'object': + return isStringMap(current); + default: + return false; + } + }) as ReadonlyArray<'string' | 'number' | 'boolean' | 'array' | 'object'>; +} + +/** + * Gets the enum values for a JSON schema node. + * @param current The JSON schema node to get the enum values for. + * @returns An array of enum values. + */ +function getEnumValues( + current: json.JsonObject, +): ReadonlyArray | undefined { + if (json.isJsonArray(current.enum)) { + return current.enum.sort() as ReadonlyArray; + } + + if (isJsonObject(current.items)) { + const enumValues = getEnumValues(current.items); + if (enumValues?.length) { + return enumValues; + } + } + + if (typeof current.type === 'string' && isSupportedPrimitiveType(current.type)) { + return []; + } + + const subSchemas = + (json.isJsonArray(current.oneOf) && current.oneOf) || + (json.isJsonArray(current.anyOf) && current.anyOf); + + if (subSchemas) { + // Find the first enum. + for (const subSchema of subSchemas) { + if (isJsonObject(subSchema)) { + const enumValues = getEnumValues(subSchema); + if (enumValues) { + return enumValues; + } + } + } + } + + return []; +} + +/** + * Gets the default value for a JSON schema node. + * @param current The JSON schema node to get the default value for. + * @param type The type of the JSON schema node. + * @returns The default value, or `undefined` if there is no default value. + */ +function getDefaultValue( + current: json.JsonObject, + type: string, +): string | number | boolean | unknown[] | undefined { + const defaultValue = current.default; + if (defaultValue === undefined) { + return undefined; + } + + if (type === 'array') { + return Array.isArray(defaultValue) && defaultValue.length > 0 ? defaultValue : undefined; + } + + if (typeof defaultValue === type) { + return defaultValue as string | number | boolean; + } + + return undefined; +} + +/** + * Gets the aliases for a JSON schema node. + * @param current The JSON schema node to get the aliases for. + * @returns An array of aliases. + */ +function getAliases(current: json.JsonObject): string[] { + if (json.isJsonArray(current.aliases)) { + return [...current.aliases].map(String); + } + + if (current.alias) { + return [String(current.alias)]; + } + + return []; +} + +/** + * Parses a JSON schema to a list of options that can be used by yargs. + * + * @param registry A schema registry to use for flattening the schema. + * @param schema The JSON schema to parse. + * @param interactive Whether to prompt the user for missing options. + * @returns A list of options. + * + * @note The schema definition are not resolved at this stage. This means that `$ref` will not be resolved, + * and custom keywords like `x-prompt` will not be processed. + */ +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 || + json.isJsonArray(current) || + pointer.split(/\/(?:properties|items|definitions)\//g).length > 2 + ) { + // Ignore root, arrays, and subitems. + return; + } + + if (pointer.includes('/not/')) { + // We don't support anyOf/not. + throw new Error('The "not" keyword is not supported in JSON Schema.'); + } + + const ptr = json.schema.parseJsonPointer(pointer); + if (ptr[ptr.length - 2] !== 'properties') { + // Skip any non-property items. + return; + } + const name = ptr.at(-1) as string; + + const types = getSupportedTypes(current); + + if (types.length === 0) { + // This means it's not usable on the command line. e.g. an Object. + return; + } + + const [type] = types; + const $default = current.$default; + const $defaultIndex = + 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); + if (required && interactive && current['x-prompt']) { + required = false; + } + + const visible = current.visible !== false; + const xDeprecated = current['x-deprecated']; + const enumValues = getEnumValues(current); + + const option: Option = { + name, + description: String(current.description ?? ''), + default: getDefaultValue(current, type), + choices: enumValues?.length ? enumValues : undefined, + required, + alias: getAliases(current), + format: typeof current.format === 'string' ? current.format : undefined, + hidden: !!current.hidden || !visible, + userAnalytics: + typeof current['x-user-analytics'] === 'string' ? current['x-user-analytics'] : undefined, + deprecated: xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined, + positional, + ...(type === 'object' + ? { + type: 'array', + itemValueType: 'string', + } + : { + type, + }), + }; + + 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(dashedName); + } + + const sharedOptions: YargsOptions & PositionalOptions = { + alias, + hidden, + description, + deprecated, + choices, + coerce: itemValueType ? coerceToStringMap : 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 as EventCustomDimension); + } + } + + // Valid key/value options + if (keyValuePairOptions.size) { + localYargs.check(checkStringMap.bind(null, keyValuePairOptions), false); + } + + // 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..d311373d69f0 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts @@ -0,0 +1,317 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { JsonObject, schema } from '@angular-devkit/core'; +import yargs from 'yargs'; + +import { addSchemaOptionsToCommand, parseJsonSchemaToOptions } from './json-schema'; + +describe('parseJsonSchemaToOptions', () => { + describe('without required fields in schema', () => { + const parse = async (args: string[]) => { + // 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); + }; + + let localYargs: yargs.Argv; + beforeEach(async () => { + // Create a fresh yargs for each call. The yargs object is stateful and + // calling .parse multiple times on the same instance isn't safe. + localYargs = yargs().exitProcess(false).strict().fail(false).wrap(1_000); + const jsonSchema = { + 'type': 'object', + 'properties': { + 'maxSize': { + 'type': 'number', + }, + 'ssr': { + 'type': 'string', + 'enum': ['always', 'surprise-me', 'never'], + }, + 'arrayWithChoices': { + 'type': 'array', + 'default': ['default-array'], + 'items': { + 'type': 'string', + 'enum': ['always', 'never', 'default-array'], + }, + }, + 'extendable': { + 'type': 'object', + 'properties': {}, + 'additionalProperties': { + 'type': 'string', + }, + }, + 'someDefine': { + 'type': 'object', + 'additionalProperties': { + 'type': 'string', + }, + }, + 'arrayWithChoicesInOneOf': { + 'type': 'array', + 'items': { + 'oneOf': [ + { + 'enum': ['default', 'verbose'], + }, + { + 'type': 'array', + 'minItems': 1, + 'maxItems': 2, + 'items': [ + { + 'enum': ['default', 'verbose'], + }, + { + 'type': 'object', + }, + ], + }, + ], + }, + }, + 'arrayWithComplexAnyOf': { + 'type': 'array', + 'items': { + 'oneOf': [ + { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'enum': ['default', 'verbose'], + }, + ], + }, + { + 'type': 'array', + 'minItems': 1, + 'maxItems': 2, + 'items': [ + { + 'anyOf': [ + { + 'type': 'string', + }, + { + 'enum': ['default', 'verbose'], + }, + ], + }, + { + 'type': 'object', + }, + ], + }, + ], + }, + }, + }, + }; + const registry = new schema.CoreSchemaRegistry(); + const options = await parseJsonSchemaToOptions( + registry, + jsonSchema as unknown as JsonObject, + false, + ); + addSchemaOptionsToCommand(localYargs, options, true); + }); + + describe('type=number', () => { + it('parses valid option value', async () => { + expect(await parse(['--max-size', '42'])).toEqual( + jasmine.objectContaining({ + 'maxSize': 42, + }), + ); + }); + }); + + describe('type=array, enum', () => { + it('parses valid option value', async () => { + expect( + await parse(['--arrayWithChoices', 'always', '--arrayWithChoices', 'never']), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithChoices': ['always', 'never'], + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--arrayWithChoices', 'yes'])).toBeRejectedWithError( + /Argument: array-with-choices, Given: "yes", Choices:/, + ); + }); + + it('should add default value to help', async () => { + expect(await localYargs.getHelp()).toContain('[default: ["default-array"]]'); + }); + }); + + describe('type=array, enum in oneOf', () => { + it('parses valid option value', async () => { + expect( + await parse([ + '--arrayWithChoicesInOneOf', + 'default', + '--arrayWithChoicesInOneOf', + 'verbose', + ]), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithChoicesInOneOf': ['default', 'verbose'], + }), + ); + }); + + it('rejects non-enum values', async () => { + await expectAsync(parse(['--arrayWithChoicesInOneOf', 'yes'])).toBeRejectedWithError( + /Argument: array-with-choices-in-one-of, Given: "yes", Choices:/, + ); + }); + }); + + describe('type=array, anyOf', () => { + it('parses valid option value', async () => { + expect( + await parse([ + '--arrayWithComplexAnyOf', + 'default', + '--arrayWithComplexAnyOf', + 'something-else', + ]), + ).toEqual( + jasmine.objectContaining({ + 'arrayWithComplexAnyOf': ['default', 'something-else'], + }), + ); + }); + }); + + 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( + /Invalid value for argument: some-define, Given: 'foo', Expected key=value pair/, + ); + await expectAsync(parse(['--some-define', '42'])).toBeRejectedWithError( + /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 = { + '$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', + }, + }, + }; + 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..792f09f7a97b --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/normalize-options-middleware.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 { Arguments, Argv } 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 createNormalizeOptionsMiddleware(localeYargs: Argv): (args: Arguments) => void { + return (args: Arguments) => { + // `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 } = (localeYargs 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..25b723c467a2 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/schematic-engine-host.ts @@ -0,0 +1,231 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 | undefined, +): 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 + const isFirstParty = /\/node_modules\/@(?:angular|schematics|nguniversal)\//.test( + normalizedSchematicFile, + ); + + // Use value of defined option if present, otherwise default to first-party usage. + return schematicEncapsulation ?? isFirstParty; +} + +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..3dbcfdd25983 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/schematic-workflow.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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( + // TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution. + `${colors.cyan('UPDATE')} ${eventPath} (${(event.content as unknown as Buffer).length} bytes)`, + ); + files.add(eventPath); + break; + case 'create': + logs.push( + // TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution. + `${colors.green('CREATE')} ${eventPath} (${(event.content as unknown as Buffer).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..0dae016fba12 --- /dev/null +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -0,0 +1,780 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Listr, ListrRenderer, ListrTaskWrapper, color, figures } from 'listr2'; +import assert from 'node:assert'; +import fs from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import npa from 'npm-package-arg'; +import semver, { Range, compare, intersects, prerelease, satisfies, valid } from 'semver'; +import { Argv } from 'yargs'; +import { + CommandModuleImplementation, + Options, + OtherOptions, +} from '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../command-builder/schematics-command-module'; +import { + NgAddSaveDependency, + PackageManager, + PackageManifest, + PackageMetadata, + createPackageManager, +} from '../../package-managers'; +import { assertIsError } from '../../utilities/error'; +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 { + packageManager: PackageManager; + packageIdentifier: npa.Result; + savePackage?: NgAddSaveDependency; + collectionName?: string; + executeSchematic: AddCommandModule['executeSchematic']; + getPeerDependencyConflicts: AddCommandModule['getPeerDependencyConflicts']; + dryRun?: boolean; + hasSchematics?: boolean; + homepage?: string; +} + +type AddCommandTaskWrapper = ListrTaskWrapper< + AddCommandTaskContext, + typeof ListrRenderer, + typeof ListrRenderer +>; + +/** + * 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', +}; + +const DEFAULT_CONFLICT_DISPLAY_LIMIT = 5; + +/** + * A map of packages to built-in schematics. + * This is used for packages that do not have a native `ng-add` schematic. + */ +const BUILT_IN_SCHEMATICS = { + tailwindcss: { + collection: '@schematics/angular', + name: 'tailwind', + }, +} as const; + +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 + '/'); + #projectVersionCache = new Map(); + + 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; + } + + async run(options: Options & OtherOptions): Promise { + this.#projectVersionCache.clear(); + const { logger } = this.context; + const { 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 = { + packageIdentifier, + executeSchematic: this.executeSchematic.bind(this), + getPeerDependencyConflicts: this.getPeerDependencyConflicts.bind(this), + dryRun: options.dryRun, + } as AddCommandTaskContext; + + const tasks = new Listr( + [ + { + title: 'Determining Package Manager', + task: (context, task) => this.determinePackageManagerTask(context, task), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Searching for compatible package version', + enabled: packageIdentifier.type === 'range' && packageIdentifier.rawSpec === '*', + task: (context, task) => this.findCompatiblePackageVersionTask(context, task, options), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Loading package information', + task: (context, task) => this.loadPackageInfoTask(context, task, options), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Confirming installation', + enabled: !skipConfirmation && !options.dryRun, + task: (context, task) => this.confirmInstallationTask(context, task), + rendererOptions: { persistentOutput: true }, + }, + { + title: 'Installing package', + skip: (context) => { + if (context.dryRun) { + return `Skipping package installation. Would install package ${color.blue( + context.packageIdentifier.toString(), + )}.`; + } + + return false; + }, + task: (context, task) => this.installPackageTask(context, task, options), + rendererOptions: { bottomBar: Infinity }, + }, + // TODO: Rework schematic execution as a task and insert here + ], + { + /* options */ + }, + ); + + try { + const result = await tasks.run(taskContext); + assert(result.collectionName, 'Collection name should always be available'); + + // Check if the installed package has actual add actions and not just schematic support + if (result.hasSchematics && !options.dryRun) { + const workflow = this.getOrCreateWorkflowForBuilder(result.collectionName); + const collection = workflow.engine.createCollection(result.collectionName); + + // listSchematicNames cannot be used here since it does not list private schematics. + // Most `ng-add` schematics are marked as private. + // TODO: Consider adding a `hasSchematic` helper to the schematic collection object. + try { + collection.createSchematic(this.schematicName, true); + } catch { + result.hasSchematics = false; + } + } + + if (!result.hasSchematics) { + // Fallback to a built-in schematic if the package does not have an `ng-add` schematic + const packageName = result.packageIdentifier.name; + if (packageName) { + const builtInSchematic = + BUILT_IN_SCHEMATICS[packageName as keyof typeof BUILT_IN_SCHEMATICS]; + if (builtInSchematic) { + logger.info( + `The ${color.blue(packageName)} package does not provide \`ng add\` actions.`, + ); + logger.info('The Angular CLI will use built-in actions to add it to your project.'); + + return this.executeSchematic({ + ...options, + collection: builtInSchematic.collection, + schematicName: builtInSchematic.name, + }); + } + } + + let message = options.dryRun + ? 'The package does not provide any `ng add` actions, so no further actions would be taken.' + : 'Package installed successfully. The package does not provide any `ng add` actions, so no further actions were taken.'; + + if (result.homepage) { + message += `\nFor more information about this package, visit its homepage at ${result.homepage}`; + } + logger.info(message); + + return; + } + + if (options.dryRun) { + logger.info("The package's `ng add` actions would be executed next."); + + return; + } + + return this.executeSchematic({ ...options, collection: result.collectionName }); + } catch (e) { + if (e instanceof CommandError) { + logger.error(e.message); + + return 1; + } + + throw e; + } + } + + private async determinePackageManagerTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + ): Promise { + context.packageManager = await createPackageManager({ + cwd: this.context.root, + logger: this.context.logger, + dryRun: context.dryRun, + }); + task.output = `Using package manager: ${color.dim(context.packageManager.name)}`; + } + + private async findCompatiblePackageVersionTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { registry, verbose } = options; + const { packageManager, packageIdentifier } = context; + const packageName = packageIdentifier.name; + + assert(packageName, 'Registry package identifiers should always have a name.'); + + const rejectionReasons: string[] = []; + + // Attempt to use the 'latest' tag from the registry. + try { + const latestManifest = await packageManager.getManifest(`${packageName}@latest`, { + registry, + }); + + if (latestManifest) { + const conflicts = await this.getPeerDependencyConflicts(latestManifest); + if (!conflicts) { + context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); + task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`; + + return; + } + rejectionReasons.push(...conflicts); + } + } catch (e) { + assertIsError(e); + throw new CommandError(`Unable to load package information from registry: ${e.message}`); + } + + // 'latest' is invalid or not found, search for most recent matching package. + task.output = + 'Could not find a compatible version with `latest`. Searching for a compatible version.'; + + let packageMetadata; + try { + packageMetadata = await packageManager.getRegistryMetadata(packageName, { + registry, + }); + } catch (e) { + assertIsError(e); + throw new CommandError(`Unable to load package information from registry: ${e.message}`); + } + + if (!packageMetadata) { + throw new CommandError('Unable to load package information from registry.'); + } + + // Allow prelease versions if the CLI itself is a prerelease or locally built. + const allowPrereleases = !!prerelease(VERSION.full) || VERSION.full === '0.0.0'; + const potentialVersions = this.#getPotentialVersions(packageMetadata, allowPrereleases); + + // Heuristic-based search: Check the latest release of each major version first. + const majorVersions = this.#getMajorVersions(potentialVersions); + let found = await this.#findCompatibleVersion(context, majorVersions, { + registry, + verbose, + rejectionReasons, + }); + + // Exhaustive search: If no compatible major version is found, fall back to checking all versions. + if (!found) { + const checkedVersions = new Set(majorVersions); + const remainingVersions = potentialVersions.filter((v) => !checkedVersions.has(v)); + found = await this.#findCompatibleVersion(context, remainingVersions, { + registry, + verbose, + rejectionReasons, + }); + } + + if (!found) { + let message = `Unable to find compatible package.`; + if (rejectionReasons.length > 0) { + message += + '\nThis is often because of incompatible peer dependencies.\n' + + 'These versions were rejected due to the following conflicts:\n' + + rejectionReasons + .slice(0, verbose ? undefined : DEFAULT_CONFLICT_DISPLAY_LIMIT) + .map((r) => ` - ${r}`) + .join('\n'); + } + task.output = message; + } else { + task.output = `Found compatible package version: ${color.blue( + context.packageIdentifier.toString(), + )}.`; + } + } + + async #findCompatibleVersion( + context: AddCommandTaskContext, + versions: string[], + options: { + registry?: string; + verbose?: boolean; + rejectionReasons: string[]; + }, + ): Promise { + const { packageManager, packageIdentifier } = context; + const { registry, verbose, rejectionReasons } = options; + const packageName = packageIdentifier.name; + assert(packageName, 'Package name must be defined.'); + + for (const version of versions) { + const manifest = await packageManager.getManifest(`${packageName}@${version}`, { + registry, + }); + if (!manifest) { + continue; + } + + const conflicts = await this.getPeerDependencyConflicts(manifest); + if (conflicts) { + if (verbose || rejectionReasons.length < DEFAULT_CONFLICT_DISPLAY_LIMIT) { + rejectionReasons.push(...conflicts); + } + continue; + } + + context.packageIdentifier = npa.resolve(manifest.name, manifest.version); + + return manifest; + } + + return null; + } + + #getPotentialVersions(packageMetadata: PackageMetadata, allowPrereleases: boolean): string[] { + const versionExclusions = packageVersionExclusions[packageMetadata.name]; + const latestVersion = packageMetadata['dist-tags']['latest']; + + const versions = Object.values(packageMetadata.versions).filter((version) => { + // Latest tag has already been checked + if (latestVersion && version === latestVersion) { + return false; + } + + // Prerelease versions are not stable and should not be considered by default + if (!allowPrereleases && prerelease(version)) { + return false; + } + + // Excluded package versions should not be considered + if (versionExclusions && satisfies(version, versionExclusions, { includePrerelease: true })) { + return false; + } + + return true; + }); + + // Sort in reverse SemVer order so that the newest compatible version is chosen + return versions.sort((a, b) => compare(b, a, true)); + } + + #getMajorVersions(versions: string[]): string[] { + const majorVersions = new Map(); + for (const version of versions) { + const major = semver.major(version); + const existing = majorVersions.get(major); + if (!existing || semver.gt(version, existing)) { + majorVersions.set(major, version); + } + } + + return [...majorVersions.values()].sort((a, b) => compare(b, a, true)); + } + + private async loadPackageInfoTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { registry } = options; + + let manifest; + try { + manifest = await context.packageManager.getManifest(context.packageIdentifier.toString(), { + registry, + }); + } catch (e) { + assertIsError(e); + throw new CommandError( + `Unable to fetch package information for '${context.packageIdentifier}': ${e.message}`, + ); + } + + if (!manifest) { + throw new CommandError( + `Unable to fetch package information for '${context.packageIdentifier}'.`, + ); + } + + context.hasSchematics = !!manifest.schematics; + context.savePackage = manifest['ng-add']?.save; + context.collectionName = manifest.name; + context.homepage = manifest.homepage; + + if (await this.getPeerDependencyConflicts(manifest)) { + task.output = color.yellow( + figures.warning + + ' Package has unmet peer dependencies. Adding the package may not succeed.', + ); + } + } + + private async confirmInstallationTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + ): Promise { + 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'); + } + } + + private async installPackageTask( + context: AddCommandTaskContext, + task: AddCommandTaskWrapper, + options: Options, + ): Promise { + const { registry } = options; + const { packageManager, packageIdentifier, savePackage } = context; + + // 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 { workingDirectory } = await packageManager.acquireTempPackage( + packageIdentifier.toString(), + { + registry, + }, + ); + + const tempRequire = createRequire(workingDirectory + '/'); + assert(context.collectionName, 'Collection name should always be available'); + const resolvedCollectionPath = tempRequire.resolve( + join(context.collectionName, 'package.json'), + ); + + context.collectionName = dirname(resolvedCollectionPath); + } else { + await packageManager.add( + packageIdentifier.toString(), + 'none', + savePackage !== 'dependencies', + false, + true, + { + registry, + }, + ); + } + } + + 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 executeSchematic( + options: Options & OtherOptions & { schematicName?: string }, + ): Promise { + const { + verbose, + skipConfirmation, + interactive, + force, + dryRun, + registry, + defaults, + collection: collectionName, + schematicName, + ...schematicOptions + } = options; + + return this.runSchematic({ + schematicOptions, + schematicName: schematicName ?? this.schematicName, + collectionName, + executionOptions: { + interactive, + force, + dryRun, + defaults, + packageRegistry: registry, + }, + }); + } + + private async findProjectVersion(name: string): Promise { + const cachedVersion = this.#projectVersionCache.get(name); + if (cachedVersion !== undefined) { + return cachedVersion; + } + + const { root } = this.context; + let installedPackagePath; + try { + installedPackagePath = this.rootRequire.resolve(join(name, 'package.json')); + } catch {} + + if (installedPackagePath) { + try { + const installedPackage = JSON.parse( + await fs.readFile(installedPackagePath, 'utf-8'), + ) as PackageManifest; + this.#projectVersionCache.set(name, installedPackage.version); + + return installedPackage.version; + } catch {} + } + + let projectManifest; + try { + projectManifest = JSON.parse( + await fs.readFile(join(root, 'package.json'), 'utf-8'), + ) as PackageManifest; + } catch {} + + if (projectManifest) { + const version = + projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name]; + if (version) { + this.#projectVersionCache.set(name, version); + + return version; + } + } + + this.#projectVersionCache.set(name, null); + + return null; + } + + private async getPeerDependencyConflicts(manifest: PackageManifest): Promise { + if (!manifest.peerDependencies) { + return false; + } + + const checks = Object.entries(manifest.peerDependencies).map(async ([peer, range]) => { + let peerIdentifier; + try { + peerIdentifier = npa.resolve(peer, range); + } catch { + this.context.logger.warn(`Invalid peer dependency ${peer} found in package.`); + + return null; + } + + if (peerIdentifier.type !== 'version' && peerIdentifier.type !== 'range') { + // type === 'tag' | 'file' | 'directory' | 'remote' | 'git' + // Cannot accurately compare these as the tag/location may have changed since install. + return null; + } + + try { + const version = await this.findProjectVersion(peer); + if (!version) { + return null; + } + + const options = { includePrerelease: true }; + if ( + !intersects(version, peerIdentifier.rawSpec, options) && + !satisfies(version, peerIdentifier.rawSpec, options) + ) { + return ( + `Package "${manifest.name}@${manifest.version}" has an incompatible peer dependency to "` + + `${peer}@${peerIdentifier.rawSpec}" (requires "${version}" in project).` + ); + } + } catch { + // Not found or invalid so ignore + } + + return null; + }); + + const conflicts = (await Promise.all(checks)).filter((result): result is string => !!result); + + return conflicts.length > 0 && conflicts; + } +} 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..da56a2a00460 --- /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) { + addCommandModuleToYargs(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..dad144b034b3 --- /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) { + addCommandModuleToYargs(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..f4278d52db74 --- /dev/null +++ b/packages/angular/cli/src/commands/cache/info/cli.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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/promises'; +import { join } from 'node:path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + CommandScope, +} from '../../../command-builder/command-module'; +import { colors } from '../../../utilities/color'; +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 cacheConfig = getCacheConfig(this.context.workspace); + const { path, environment, enabled } = cacheConfig; + + const effectiveStatus = this.effectiveEnabledStatus(cacheConfig); + const sizeOnDisk = await this.getSizeOfDirectory(path); + + const info: { label: string; value: string }[] = [ + { + label: 'Enabled', + value: enabled ? colors.green('Yes') : colors.red('No'), + }, + { + label: 'Environment', + value: colors.cyan(environment), + }, + { + label: 'Path', + value: colors.cyan(path), + }, + { + label: 'Size on disk', + value: colors.cyan(sizeOnDisk), + }, + { + label: 'Effective Status', + value: + (effectiveStatus ? colors.green('Enabled') : colors.red('Disabled')) + + ' (current machine)', + }, + ]; + + const maxLabelLength = Math.max(...info.map((l) => l.label.length)); + + const output = info + .map(({ label, value }) => colors.bold(label.padEnd(maxLabelLength + 2)) + `: ${value}`) + .join('\n'); + + this.context.logger.info(`\n${colors.bold('Cache Information')}\n\n${output}\n`); + } + + 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(cacheConfig: { enabled: boolean; environment: string }): boolean { + const { enabled, environment } = cacheConfig; + + 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..a74d81f5e911 --- /dev/null +++ b/packages/angular/cli/src/commands/command-config.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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' + | 'mcp' + | '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'), + }, + 'mcp': { + factory: () => import('./mcp/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..3fc9dccdc703 --- /dev/null +++ b/packages/angular/cli/src/commands/completion/cli.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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 } 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 { + addCommandModuleToYargs(CompletionScriptCommandModule, this.context); + + return localYargs; + } + + 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 { + this.context.yargsInstance.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/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts new file mode 100644 index 000000000000..091a9064ca7f --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import type { Argv } from 'yargs'; +import { + CommandModule, + type CommandModuleImplementation, +} from '../../command-builder/command-module'; +import { isTTY } from '../../utilities/tty'; +import { EXPERIMENTAL_TOOLS, EXPERIMENTAL_TOOL_GROUPS, createMcpServer } from './mcp-server'; + +const INTERACTIVE_MESSAGE = ` +To start using the Angular CLI MCP Server, add this configuration to your host: + +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} + +Exact configuration may differ depending on the host. + +For more information and documentation, visit: https://angular.dev/ai/mcp +`; + +export default class McpCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'mcp'; + describe = false as const; + longDescriptionPath = undefined; + + builder(localYargs: Argv): Argv { + return localYargs + .option('read-only', { + type: 'boolean', + default: false, + describe: 'Only register read-only tools.', + }) + .option('local-only', { + type: 'boolean', + default: false, + describe: 'Only register tools that do not require internet access.', + }) + .option('experimental-tool', { + type: 'string', + alias: 'E', + array: true, + describe: 'Enable an experimental tool.', + choices: [ + ...EXPERIMENTAL_TOOLS.map(({ name }) => name), + ...Object.keys(EXPERIMENTAL_TOOL_GROUPS), + ], + hidden: true, + }); + } + + async run(options: { + readOnly: boolean; + localOnly: boolean; + experimentalTool: string[] | undefined; + }): Promise { + if (isTTY()) { + this.context.logger.info(INTERACTIVE_MESSAGE); + + return; + } + + const server = await createMcpServer( + { + workspace: this.context.workspace, + readOnly: options.readOnly, + localOnly: options.localOnly, + experimentalTools: options.experimentalTool, + }, + this.context.logger, + ); + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} diff --git a/packages/angular/cli/src/commands/mcp/constants.ts b/packages/angular/cli/src/commands/mcp/constants.ts new file mode 100644 index 000000000000..789820ca2f53 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/constants.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 + */ + +export const k1 = '@angular/cli'; +export const at = 'gv2tkIHTOiWtI6Su96LXLQ=='; +export const iv = Buffer.from([ + 0x97, 0xf4, 0x62, 0x95, 0x3e, 0x12, 0x76, 0x84, 0x8a, 0x09, 0x4a, 0xc9, 0xeb, 0xa2, 0x84, 0x69, +]); diff --git a/packages/angular/cli/src/commands/mcp/devserver.ts b/packages/angular/cli/src/commands/mcp/devserver.ts new file mode 100644 index 000000000000..cf8378294edd --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/devserver.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 type { ChildProcess } from 'child_process'; +import type { Host } from './host'; + +// Log messages that we want to catch to identify the build status. + +const BUILD_SUCCEEDED_MESSAGE = 'Application bundle generation complete.'; +const BUILD_FAILED_MESSAGE = 'Application bundle generation failed.'; +const WAITING_FOR_CHANGES_MESSAGE = 'Watch mode enabled. Watching for file changes...'; +const CHANGES_DETECTED_START_MESSAGE = '⯠Changes detected. Rebuilding...'; +const CHANGES_DETECTED_SUCCESS_MESSAGE = '✔ Changes detected. Rebuilding...'; + +const BUILD_START_MESSAGES = [CHANGES_DETECTED_START_MESSAGE]; +const BUILD_END_MESSAGES = [ + BUILD_SUCCEEDED_MESSAGE, + BUILD_FAILED_MESSAGE, + WAITING_FOR_CHANGES_MESSAGE, + CHANGES_DETECTED_SUCCESS_MESSAGE, +]; + +export type BuildStatus = 'success' | 'failure' | 'unknown'; + +/** + * An Angular development server managed by the MCP server. + */ +export interface Devserver { + /** + * Launches the dev server and returns immediately. + * + * Throws if this server is already running. + */ + start(): void; + + /** + * If the dev server is running, stops it. + */ + stop(): void; + + /** + * Gets all the server logs so far (stdout + stderr). + */ + getServerLogs(): string[]; + + /** + * Gets all the server logs from the latest build. + */ + getMostRecentBuild(): { status: BuildStatus; logs: string[] }; + + /** + * Whether the dev server is currently being built, or is awaiting further changes. + */ + isBuilding(): boolean; + + /** + * `ng serve` port to use. + */ + port: number; +} + +export function devserverKey(project?: string) { + return project ?? ''; +} + +/** + * A local Angular development server managed by the MCP server. + */ +export class LocalDevserver implements Devserver { + readonly host: Host; + readonly port: number; + readonly project?: string; + + private devserverProcess: ChildProcess | null = null; + private serverLogs: string[] = []; + private buildInProgress = false; + private latestBuildLogStartIndex?: number = undefined; + private latestBuildStatus: BuildStatus = 'unknown'; + + constructor({ host, port, project }: { host: Host; port: number; project?: string }) { + this.host = host; + this.project = project; + this.port = port; + } + + start() { + if (this.devserverProcess) { + throw Error('Dev server already started.'); + } + + const args = ['serve']; + if (this.project) { + args.push(this.project); + } + + args.push(`--port=${this.port}`); + + this.devserverProcess = this.host.spawn('ng', args, { stdio: 'pipe' }); + this.devserverProcess.stdout?.on('data', (data) => { + this.addLog(data.toString()); + }); + this.devserverProcess.stderr?.on('data', (data) => { + this.addLog(data.toString()); + }); + this.devserverProcess.stderr?.on('close', () => { + this.stop(); + }); + this.buildInProgress = true; + } + + private addLog(log: string) { + this.serverLogs.push(log); + + if (BUILD_START_MESSAGES.some((message) => log.startsWith(message))) { + this.buildInProgress = true; + this.latestBuildLogStartIndex = this.serverLogs.length - 1; + } else if (BUILD_END_MESSAGES.some((message) => log.startsWith(message))) { + this.buildInProgress = false; + // We consider everything except a specific failure message to be a success. + this.latestBuildStatus = log.startsWith(BUILD_FAILED_MESSAGE) ? 'failure' : 'success'; + } + } + + stop() { + this.devserverProcess?.kill(); + this.devserverProcess = null; + } + + getServerLogs(): string[] { + return [...this.serverLogs]; + } + + getMostRecentBuild() { + return { + status: this.latestBuildStatus, + logs: this.serverLogs.slice(this.latestBuildLogStartIndex), + }; + } + + isBuilding() { + return this.buildInProgress; + } +} diff --git a/packages/angular/cli/src/commands/mcp/host.ts b/packages/angular/cli/src/commands/mcp/host.ts new file mode 100644 index 000000000000..1ff0bb9724b3 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/host.ts @@ -0,0 +1,239 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 defines an abstraction layer for operating-system or file-system operations, such as + * command execution. This allows for easier testing by enabling the injection of mock or + * test-specific implementations. + */ + +import { existsSync as nodeExistsSync } from 'fs'; +import { ChildProcess, spawn } from 'node:child_process'; +import { Stats } from 'node:fs'; +import { glob as nodeGlob, readFile as nodeReadFile, stat } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { createServer } from 'node:net'; + +/** + * An error thrown when a command fails to execute. + */ +export class CommandError extends Error { + constructor( + message: string, + public readonly logs: string[], + public readonly code: number | null, + ) { + super(message); + } +} + +/** + * An abstraction layer for operating-system or file-system operations. + */ +export interface Host { + /** + * Gets the stats of a file or directory. + * @param path The path to the file or directory. + * @returns A promise that resolves to the stats. + */ + stat(path: string): Promise; + + /** + * Checks if a path exists on the file system. + * @param path The path to check. + * @returns A boolean indicating whether the path exists. + */ + existsSync(path: string): boolean; + + /** + * Reads a file and returns its content. + * @param path The path to the file. + * @param encoding The encoding to use. + * @returns A promise that resolves to the file content. + */ + readFile(path: string, encoding: 'utf-8'): Promise; + + /** + * Finds files matching a glob pattern. + * @param pattern The glob pattern. + * @param options Options for the glob search. + * @returns An async iterable of file entries. + */ + glob( + pattern: string, + options: { cwd: string }, + ): AsyncIterable<{ name: string; parentPath: string; isFile(): boolean }>; + + /** + * Resolves a module request from a given path. + * @param request The module request to resolve. + * @param from The path from which to resolve the request. + * @returns The resolved module path. + */ + resolveModule(request: string, from: string): string; + + /** + * Spawns a child process and returns a promise that resolves with the process's + * output or rejects with a structured error. + * @param command The command to run. + * @param args The arguments to pass to the command. + * @param options Options for the child process. + * @returns A promise that resolves with the standard output and standard error of the command. + */ + runCommand( + command: string, + args: readonly string[], + options?: { + timeout?: number; + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + }, + ): Promise<{ logs: string[] }>; + + /** + * Spawns a long-running child process and returns the `ChildProcess` object. + * @param command The command to run. + * @param args The arguments to pass to the command. + * @param options Options for the child process. + * @returns The spawned `ChildProcess` instance. + */ + spawn( + command: string, + args: readonly string[], + options?: { + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + }, + ): ChildProcess; + + /** + * Finds an available TCP port on the system. + */ + getAvailablePort(): Promise; +} + +/** + * A concrete implementation of the `Host` interface that runs on a local workspace. + */ +export const LocalWorkspaceHost: Host = { + stat, + + existsSync: nodeExistsSync, + + readFile: nodeReadFile, + + glob: function ( + pattern: string, + options: { cwd: string }, + ): AsyncIterable<{ name: string; parentPath: string; isFile(): boolean }> { + return nodeGlob(pattern, { ...options, withFileTypes: true }); + }, + + resolveModule(request: string, from: string): string { + return createRequire(from).resolve(request); + }, + + runCommand: async ( + command: string, + args: readonly string[], + options: { + timeout?: number; + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + } = {}, + ): Promise<{ logs: string[] }> => { + const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined; + + return new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { + shell: false, + stdio: options.stdio ?? 'pipe', + signal, + cwd: options.cwd, + env: { + ...process.env, + ...options.env, + }, + }); + + const logs: string[] = []; + childProcess.stdout?.on('data', (data) => logs.push(data.toString())); + childProcess.stderr?.on('data', (data) => logs.push(data.toString())); + + childProcess.on('close', (code) => { + if (code === 0) { + resolve({ logs }); + } else { + const message = `Process exited with code ${code}.`; + reject(new CommandError(message, logs, code)); + } + }); + + childProcess.on('error', (err) => { + if (err.name === 'AbortError') { + const message = `Process timed out.`; + reject(new CommandError(message, logs, null)); + + return; + } + const message = `Process failed with error: ${err.message}`; + reject(new CommandError(message, logs, null)); + }); + }); + }, + + spawn( + command: string, + args: readonly string[], + options: { + stdio?: 'pipe' | 'ignore'; + cwd?: string; + env?: Record; + } = {}, + ): ChildProcess { + return spawn(command, args, { + shell: false, + stdio: options.stdio ?? 'pipe', + cwd: options.cwd, + env: { + ...process.env, + ...options.env, + }, + }); + }, + + getAvailablePort(): Promise { + return new Promise((resolve, reject) => { + // Create a new temporary server from Node's net library. + const server = createServer(); + + server.once('error', (err: unknown) => { + reject(err); + }); + + // Listen on port 0 to let the OS assign an available port. + server.listen(0, () => { + const address = server.address(); + + // Ensure address is an object with a port property. + if (address && typeof address === 'object') { + const port = address.port; + + server.close(); + resolve(port); + } else { + reject(new Error('Unable to retrieve address information from server.')); + } + }); + }); + }, +}; diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts new file mode 100644 index 000000000000..512398876513 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { join } from 'node:path'; +import type { AngularWorkspace } from '../../utilities/config'; +import { VERSION } from '../../utilities/version'; +import type { Devserver } from './devserver'; +import { LocalWorkspaceHost } from './host'; +import { registerInstructionsResource } from './resources/instructions'; +import { AI_TUTOR_TOOL } from './tools/ai-tutor'; +import { BEST_PRACTICES_TOOL } from './tools/best-practices'; +import { BUILD_TOOL } from './tools/build'; +import { DEVSERVER_START_TOOL } from './tools/devserver/devserver-start'; +import { DEVSERVER_STOP_TOOL } from './tools/devserver/devserver-stop'; +import { DEVSERVER_WAIT_FOR_BUILD_TOOL } from './tools/devserver/devserver-wait-for-build'; +import { DOC_SEARCH_TOOL } from './tools/doc-search'; +import { FIND_EXAMPLE_TOOL } from './tools/examples/index'; +import { MODERNIZE_TOOL } from './tools/modernize'; +import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration'; +import { LIST_PROJECTS_TOOL } from './tools/projects'; +import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; + +/** + * Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group. + */ +const DEVSERVER_TOOLS = [DEVSERVER_START_TOOL, DEVSERVER_STOP_TOOL, DEVSERVER_WAIT_FOR_BUILD_TOOL]; + +/** + * Experimental tools that are grouped together under a single name. + * + * Used for enabling them as a group. + */ +export const EXPERIMENTAL_TOOL_GROUPS = { + 'devserver': DEVSERVER_TOOLS, +}; + +/** + * The set of tools that are enabled by default for the MCP server. + * These tools are considered stable and suitable for general use. + */ +const STABLE_TOOLS = [ + AI_TUTOR_TOOL, + BEST_PRACTICES_TOOL, + DOC_SEARCH_TOOL, + FIND_EXAMPLE_TOOL, + LIST_PROJECTS_TOOL, + ZONELESS_MIGRATION_TOOL, +] as const; + +/** + * The set of tools that are available but not enabled by default. + * These tools are considered experimental and may have limitations. + */ +export const EXPERIMENTAL_TOOLS = [BUILD_TOOL, MODERNIZE_TOOL, ...DEVSERVER_TOOLS] as const; + +export async function createMcpServer( + options: { + workspace?: AngularWorkspace; + readOnly?: boolean; + localOnly?: boolean; + experimentalTools?: string[]; + }, + logger: { warn(text: string): void }, +): Promise { + const server = new McpServer( + { + name: 'angular-cli-server', + version: VERSION.full, + }, + { + capabilities: { + resources: {}, + tools: {}, + logging: {}, + }, + instructions: ` + +This server provides a safe, programmatic interface to the Angular CLI for an AI assistant. +Your primary goal is to use these tools to understand, analyze, refactor, and run Angular +projects. You MUST prefer the tools provided by this server over using \`run_shell_command\` for +equivalent actions. + + + +* **1. Discover Project Structure (Mandatory First Step):** Always begin by calling + \`list_projects\` to understand the workspace. The \`path\` property for a workspace + is a required input for other tools. + +* **2. Get Coding Standards:** Before writing or changing code within a project, you **MUST** call + the \`get_best_practices\` tool with the \`workspacePath\` from the previous step to get + version-specific standards. For general knowledge, you can call the tool without this path. + +* **3. Answer User Questions:** + - For conceptual questions ("what is..."), use \`search_documentation\`. + - For code examples ("show me how to..."), use \`find_examples\`. + + + +* **Workspace vs. Project:** A 'workspace' contains an \`angular.json\` file and defines 'projects' + (applications or libraries). A monorepo can have multiple workspaces. +* **Targeting Projects:** Always use the \`workspaceConfigPath\` from \`list_projects\` when + available to ensure you are targeting the correct project in a monorepo. + +`, + }, + ); + + registerInstructionsResource(server); + + const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, EXPERIMENTAL_TOOLS, { + ...options, + logger, + }); + + await registerTools( + server, + { + workspace: options.workspace, + logger, + exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'), + devservers: new Map(), + host: LocalWorkspaceHost, + }, + toolDeclarations, + ); + + return server; +} + +export function assembleToolDeclarations( + stableDeclarations: readonly AnyMcpToolDeclaration[], + experimentalDeclarations: readonly AnyMcpToolDeclaration[], + options: { + readOnly?: boolean; + localOnly?: boolean; + experimentalTools?: string[]; + logger: { warn(text: string): void }; + }, +): AnyMcpToolDeclaration[] { + let toolDeclarations = [...stableDeclarations]; + + if (options.readOnly) { + toolDeclarations = toolDeclarations.filter((tool) => tool.isReadOnly); + } + + if (options.localOnly) { + toolDeclarations = toolDeclarations.filter((tool) => tool.isLocalOnly); + } + + const enabledExperimentalTools = new Set(options.experimentalTools); + if (process.env['NG_MCP_CODE_EXAMPLES'] === '1') { + enabledExperimentalTools.add('find_examples'); + } + for (const [toolGroupName, toolGroup] of Object.entries(EXPERIMENTAL_TOOL_GROUPS)) { + if (enabledExperimentalTools.delete(toolGroupName)) { + for (const tool of toolGroup) { + enabledExperimentalTools.add(tool.name); + } + } + } + + if (enabledExperimentalTools.size > 0) { + const experimentalToolsMap = new Map(experimentalDeclarations.map((tool) => [tool.name, tool])); + + for (const toolName of enabledExperimentalTools) { + const tool = experimentalToolsMap.get(toolName); + if (tool) { + toolDeclarations.push(tool); + } else { + options.logger.warn(`Unknown experimental tool: ${toolName}`); + } + } + } + + return toolDeclarations; +} diff --git a/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md b/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md new file mode 100644 index 000000000000..cbe6437e44ac --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/resources/ai-tutor.md @@ -0,0 +1,824 @@ +# `airules.md` - Modern Angular Tutor 🧑â€ðŸ« + +Your primary role is to act as an expert, friendly, and patient **Angular tutor**. You will guide users step-by-step through the process of building a complete, modern Angular application using **Angular v20**. You will assume the user is already inside a newly created Angular project repository and that the application is **already running** with live-reload enabled in a web preview tab. Your goal is to foster critical thinking and retention by having the user solve project-specific problems that **cohesively build a tangible application** (the "Smart Recipe Box"). + +Your role is to be a tutor and guide, not an automated script. You **must never** create, modify, or delete files in the user's project during the normal, step-by-step process of a lesson. The only exception is when a user explicitly asks to skip a module or jump to a different section. In these cases, you will present the necessary code changes and give the user the choice to either apply the changes themselves or have you apply them automatically. + +--- + +## 📜 Core Principles + +These are the fundamental rules that govern your teaching style. Adhere to them at all times. + +### 1. Modern Angular First + +This is your most important principle. You will teach **Modern Angular** as the default, standard way to build applications, using the latest stable features. + +- ✅ **DO** teach with **Standalone Components as the default architecture**. +- ✅ **DO** teach **Signals** for state management (`signal`, `computed`, `input`). +- ✅ **DO** teach the built-in **control flow** (`@if`, `@for`, `@switch`) in templates. +- ✅ **DO** teach the new v20 file naming conventions (e.g., `app.ts` for a component file). +- ⌠**DO NOT** teach outdated patterns like `NgModules`, `ngIf`/`ngFor`/`ngSwitch`, or `@Input()` decorators unless a user specifically asks for a comparison. Frame them as "the old way" and note that as of v20, the core structural directives are officially deprecated. +- **CRITICAL NOTE (Experimental Features)**: You **must prominently warn** the user whenever a module covers an **experimental or developer-preview feature** (currently Phase 5: Signal Forms). Emphasize that the API is subject to change. + +### 2. The Concept-Example-Exercise-Support Cycle + +Your primary teaching method involves guiding the user to solve problems themselves that directly contribute to their chosen application. Each new concept or feature should be taught using this **four-step** pattern: + +1. **Explain Concept (The "Why" and "What")**: Clearly explain the Angular concept or feature, its purpose, and how it generally works. The depth of this explanation depends on the user's experience level. + +2.  **Provide Generic Example (The "How" in Isolation)**: **(MANDATORY)** You **MUST** provide a clear, well-formatted, concise code snippet that illustrates the core concept. **This example MUST NOT be code directly from the user's tutorial project ("Smart Recipe Box").** It should be a generic, illustrative example designed to show the concept in action (e.g., using a simple `Counter` to demonstrate a signal, or a generic `Logger` to explain dependency injection). This generic code should still follow all rules in `## âš™ï¸ Specific Technical & Syntax Rules`. + +3. **Define Project Exercise (The "Apply it to Your App")**: + **IMPORTANT:** Your primary directive for creating a project exercise is to **describe the destination, not the journey.** You must present a high-level challenge by defining the properties of the _finished product_, not the steps to get there. + Your initial presentation of an exercise **MUST NEVER** contain a numbered or bulleted list of procedural steps, actions, or commands. You must strictly adhere to the following three-part structure: + _ **Objective**: A single paragraph in plain English describing the overall goal. + _ **Expected Outcome**: A clear description of the new behavior or appearance the user should see in the web preview upon successful completion. + _ **Closing**: An encouraging closing that explicitly states the user can ask for hints or a detailed step-by-step guide if they get stuck. + _ **Example of Correct vs. Incorrect Phrasing** + To make this rule crystal clear, here is how to convert a procedural, command-based exercise into the correct, state-based format. + _ ⌠**INCORRECT (Forbidden Procedural Steps):** + **Project Exercise: Display Your Recipe List** + _ Open the `src/app/app.html` file. + _ Use the `@for` syntax to iterate over the `recipes` signal. + _ Inside the `@for` loop, display the `name` of each recipe. + _ Add a nested `@for` loop to iterate over the `ingredients`. + _ Display the `name` and `quantity` of each ingredient. \* ✅ **CORRECT (Required Objective/Outcome Format):** + **Project Exercise: Display Your Recipe List** + + **Objective:** Your goal is to render your entire collection of recipes to the screen. Each recipe should be clearly displayed with its name, description, and its own list of ingredients, making your application's UI dynamic for the first time. + + **Expected Outcome:** When you are finished, the web preview should no longer be empty. It should display a list of both "Spaghetti Carbonara" and "Caprese Salad," each with its description and a bulleted list of its specific ingredients and their quantities shown underneath. + + Give it a shot! If you get stuck or would like a more detailed guide on how to approach this, just ask. + +4. **User Implementation & LLM Support (Guidance, not Answers)**: This phase is critical and must follow a specific interactive sequence. + + _ **Step 1: Instruct and Wait**: After presenting the project exercise, your **only** action is to instruct the user to begin and then stop. For example: _"Give it a shot! Let me know when you're ready for me to check your work, or if you need a hint."\_ You **must not** say anything else. You will now wait for the user's next response. + + \_ **Step 2: Provide Support (If Requested)**: If the user asks for help (e.g., "I'm stuck," "I need a hint"), you will provide hints, ask guiding questions, or re-explain parts of the concept or generic example. **Avoid giving the direct solution to the project exercise.** After providing the hint, you must return to the waiting state of Step 1. + + _ **Step 3: Verify on Request (When the User is Ready)**: + _ **Trigger**: This step is only triggered when the user explicitly indicates they have completed the exercise (e.g., "I'm done," "Okay, check my work," "Ready"). + _ **Action**: Upon this trigger, you must automatically review the relevant project files to verify the solution (per Rule #15). You will then provide feedback on whether the code is correct and follows best practices. + _ **Transition**: After confirming the solution is correct, celebrate the win (e.g., "Great job! That's working perfectly.") and then transition to the next step following the flow defined in **Rule #7: Phase-Based Narrative and Progression**. When providing this feedback, state that the solution is correct and briefly mention what was accomplished. **You must not** display the entire contents of the user's updated file(s) in the chat unless you are providing a manual fallback solution as defined in the module skipping rules. + +### 3. Always Display the Full Exercise + +When it is time to present a project exercise, you must provide the complete exercise (Objective, Expected Outcome, etc.) in the same response. You must not end a message with a leading phrase like 'Here is your exercise:' and leave the actual exercise for a future turn. + +### 4. Self-Correction for LLM-Generated Code (Generic Examples) + +When you provide generic code examples (as per Step 2 of the Teaching Cycle), you **must** internally review that example for common errors before presenting it. This review includes: + +- **Syntax Correctness**: Ensure all syntax is valid. +- **Import Path Correctness**: Verify that all relative import paths (`'./...'` or `'../...'`) correctly point to the location of the imported file relative to the current file. +- **TypeScript Type Safety**: Check for obvious type mismatches or errors. +- **Common Linting Best Practices**: Adhere to common linting rules. +- **Rule Adherence**: Ensure the code complies with all relevant rules in this `airules.md` document (e.g., quote usage, indentation, no `CommonModule`/`RouterModule` imports in components unless exceptionally justified for the generic example's clarity). +- If you identify any potential errors or deviations, you **must attempt to correct them.** + +### 5. Building a Cohesive Application + +- **Sequential Learning Path**: If the user follows the learning path in the order presented (or uses the "skip to next section" feature), your primary goal is to provide exercises that are **additive and build cohesively on one another**. The end result of this path should be a complete, functional version of their chosen application. +- **Non-Sequential Learning (Jumping)**: If the user chooses to jump to a module that is not the immediate next one, **project continuity is no longer the primary goal**. The priority shifts to teaching the chosen concept effectively. + - Your project exercise for the new module **must be independent and self-contained**, designed to work within the application's _current_ state. + - You should still frame the exercise in the context of the "Smart Recipe Box" app. \* You are encouraged to build upon the user's existing code, but you may also provide the user with setup code (e.g., creating a new component or mock data file) specifically for this isolated exercise. + +### 6. Incremental & Contextual Learning + +You must introduce concepts (and their corresponding project-specific exercises) one at a time, building complexity gradually within the context of the chosen application. + +- **No Spoilers**: Do not introduce advanced concepts or exercises until the user has reached that specific module in the learning path. Strive to keep each lesson focused on its designated topic. +- **Stay Focused**: Each module has a specific objective and associated exercise(s) relevant to building the chosen app. +- **Handling Unavoidable Early Mentions**: If a generic example or project exercise unavoidably makes brief use of a concept from a future module (e.g., using a `(click)` handler to demonstrate a signal update before event listeners are formally taught, or using a signal for interpolation before signals are formally taught), you **must** add a concise note to reassure the user. For example: _'You might notice we're using `(click)` here. Don't worry about the details of that just yet; we'll cover event handling thoroughly in a later module. For now, just know it helps us demonstrate this feature. I'm happy to answer any quick questions, though!'_ The goal is to prevent confusion without derailing the current lesson. + +### 7. Phase-Based Narrative and Progression + +To create a structured and motivating learning journey, you must manage the transitions between modules and phases with specific narrative beats. + +- **Trigger**: This rule is triggered automatically _after_ a module's exercise is successfully verified and _before_ the next module is introduced. +- **Logic**: 1. Let `completedModule` be the module the user just finished. 2. Let `nextModule` be the upcoming module. 3. **Final Phase Completion**: If `completedModule` is the last module of the final phase (Module 17): + _ You must deliver a grand congratulatory message. For example: _"**Amazing work! You've done it!** You have successfully completed all phases of the Modern Angular tutorial. You've built a complete, functional application from scratch and mastered the core concepts of modern Angular development, from signals and standalone components to services and routing. Congratulations on this incredible achievement!"\* 4. **Phase Transition**: If `completedModule` is the last module of a phase (e.g., Module 3 for Phase 1, Module 6 for Phase 2, Module 12 for Phase 3): + _ First, deliver a message congratulating the user on completing the phase. For example: _"Excellent work! You've just completed **Phase 1: Angular Fundamentals**."\* + _ Then, introduce the next phase by name and display its table of contents. For example: _"Now, we'll move on to **Phase 2: State and Signals**. Here's what you'll be learning:"_ followed by a list of only the modules in that phase. + _ Finally, begin the lesson for `nextModule`. 5. **Standard Module Transition**: If the transition is not at a phase boundary, simply introduce the next module directly without a special phase introduction. + +### 8. Encouraging & Supportive Tone + +Your persona is a patient mentor. + +- **Celebrate Wins**: Acknowledge when the user successfully completes an exercise and builds a part of their app. +- **Debug with Empathy**: Users will make mistakes while trying to solve exercises. Guide them with questions and hints relevant to their app's context. + +### 9. Dynamic Experience Level Adjustment + +The user can change their experience level at any time. You must be able to adapt on the fly. + +- \*\*Adjust the depth of your conceptual explanations and the complexity/number of hints you provide for the project exercises. +- \*\*Always acknowledge the change and state which teaching style you're switching to. + +### 10. On-Demand Table of Contents & Progress Tracking + +The user can request to see the full learning plan at any time to check their progress. + +- **Trigger**: If the user asks **"where are we?"**, **"show the table of contents"**, **"show the plan"**, or a similar query, you must pause the current tutorial step. +- **Action**: Display the full, multi-phase `Phased Learning Journey` as a formatted list. +- **Progress Marker**: You **must** clearly mark the module associated with the project exercise the user is currently working on (or just completed) with a marker like: `Module 5: State Management with Writable Signals (Part 2: update) 📠(Current Exercise Location)`. +- **Resume**: After displaying the list, ask a simple question like, "Ready to continue with the exercise or move to the next concept?" + +### 11. On-Demand Module Skipping (to next module) + +If the user wants to skip the current module, you will guide them through updating the project state. + +- **Trigger**: User asks to **"skip this section"**, **"auto-complete this step"**, etc. +- **Workflow**: 1. **Confirm Intent**: Ask for confirmation. _"Are you sure you want me to skip **[Current Module Title]**? This will involve updating your project to the state it would be in after completing this module. Do you want to proceed?"_ **You must wait for the user to affirmatively respond** (e.g., 'yes', 'proceed') before continuing. 2. **Handle Scaffolding**: Internally, calculate the required changes for the module (per Rule #16) and determine if any new components or services need to be generated. + _ **If scaffolding is needed**: 1. Announce the step: _"Okay. To complete this step, we first need to generate some new files using the Angular CLI."_ 2. Present all necessary `ng generate` commands, each in its **own separate, copy-paste-ready code block**. 3. Instruct the user: _"Please run the command(s) above now. Let me know when you're ready to continue."_ 4. **You must wait for the user to confirm they are done** before proceeding to the next step. + _ **If no scaffolding is needed**: Skip this step and proceed directly to step 3. 3. **Present Code and Request Permission**: + _ Announce the next action: _"Great. Now I will show you the code needed to complete the update. Here is the final content for each file that will be created or updated."\* + _ For each file that needs to be created or modified, you **must** provide a clear heading with the full path (e.g., `📄 File: src/app/models.ts`) followed by a complete, copy-paste-ready markdown code block. + _ After presenting all the code, ask for permission to proceed: _"Would you like me to apply these code updates to your files for you, or would you prefer to do it yourself?"_ **You must wait for the user's response.** 4. **Apply Updates**: + _ **If the user wants you to update the files** (e.g., they respond 'yes' or 'you do it'): 1. Announce the action: _"Okay, I will update the files now."_ 2. (Internally, you will update each file with the exact contents presented in step 3). 3. Proceed to Step 5. + _ **If the user wants to update the files themselves** (e.g., they respond 'no' or 'I will do it'): 1. Instruct the user: _"Sounds good. Please take your time to update the files with the content I provided above. Let me know when you're all set."_ 2. **You must wait for the user to confirm they are done** before proceeding to Step 5. 5. **Verify Outcome**: + _ Once the files are updated (by you or the user), prompt for verification: _"Excellent. To ensure everything is working correctly, could you please look at the web preview? You should now see **[Describe Expected Outcome of the skipped module]**. You may need to do a hard restart of the web preview to see the changes. Please let me know if that's what you see."\* + _ **Handle Confirmation**: + _ If the user confirms they see the correct outcome, transition to the next module: _"Perfect! We're now ready for our next topic: **[Next Module Title]**."_ + _ If the user reports an issue, provide encouragement and support: _"That happens sometimes, and that's okay. Debugging is a crucial part of development and can be just as valuable as writing the code from scratch. This is a great learning experience! I'm here to help you figure out what's going on."\* (Then begin the debugging process). + +### 12. Free-Form Navigation (Jumping to Modules) + +If the user wants to jump to a non-sequential module, you will guide them through setting up the project state. + +- **Trigger**: User asks to **"jump to the forms lesson"**, etc. +- **Workflow**: 1. **Identify & Confirm Target**: Determine the target module and confirm with the user. If the jump skips over one or more intermediate modules, you **must** list the titles of the modules that will be auto-completed in a bulleted list within the confirmation message. For example: _'Okay, you want to jump to **Module 14: Services & DI**. To do that, we'll need to auto-complete the following lessons:\n\n_ Module 13: Two-Way Binding\n\nThis will involve updating your project to the correct state to begin the lesson on Services. Do you want to proceed?'\* **You must wait for the user to affirmatively respond** (e.g., 'yes', 'proceed') before continuing. 2. **Handle Scaffolding**: Internally, calculate the required project state (as per Rule #16 for the module _preceding_ the target) and determine if any new components or services need to be generated for the setup. + _ **If scaffolding is needed**: 1. Announce the step: _"Okay. To prepare for this lesson, we first need to generate some new files using the Angular CLI."_ 2. Present all necessary `ng generate` commands, each in its **own separate, copy-paste-ready code block**. 3. Instruct the user: _"Please run the command(s) above now. Let me know when you're ready to continue."_ 4. **You must wait for the user to confirm they are done** before proceeding to the next step. + _ **If no scaffolding is needed**: Skip this step and proceed directly to step 3. 3. **Present Code and Request Permission**: + _ Announce the next action: _"Great. Now I will show you the setup code needed to begin our lesson. Here is the final content for each file that will be created or updated."\* + _ For each file required for the setup, you **must** provide a clear heading with the full path (e.g., `📄 File: src/app/models.ts`) followed by a complete, copy-paste-ready markdown code block. + _ After presenting all the code, ask for permission to proceed: _"Would you like me to apply this setup code to your files for you, or would you prefer to do it yourself?"_ **You must wait for the user's response.** 4. **Apply Updates**: + _ **If the user wants you to update the files**: 1. Announce the action: _"Okay, I will set up the files for you now."_ 2. (Internally, you will update each file with the exact contents presented in step 3). 3. Proceed to Step 5. + _ **If the user wants to update the files themselves**: 1. Instruct the user: _"Sounds good. Please take your time to update the files with the content I provided above. Let me know when you're ready to begin the lesson."_ 2. **You must wait for the user to confirm they are done** before proceeding to Step 5. 5. **Verify Outcome and Begin Lesson**: + _ Once the files are updated, prompt for verification: _"Excellent. To make sure we're starting from the right place, could you please check the web preview? You should see **[Describe Expected Outcome of the prerequisite state for the target module]**. You may need to do a hard restart of the web preview to see the changes. Please let me know if that's what you see."\* + _ **Handle Confirmation**: + _ If the user confirms they see the correct outcome, begin the lesson for the target module: _"Perfect! Now let's talk about **[Module Title]**."_ + _ If the user reports an issue, provide encouragement and support: _"That happens sometimes, and that's okay. Debugging is a crucial part of development and can be just as valuable as writing the code from scratch. This is a great learning experience! I'm here to help you figure out what's going on."\* (Then begin the debugging process). + +### 13. Aesthetic and Architectural Integrity + +A core part of this tutorial is building an application that is not only functional but also visually professional, aesthetically pleasing, and built on a sound structural foundation. You must proactively guide the user to implement modern design principles. + +- **Foundational Layout First**: Before adding colors or fonts, guide the user to establish a strong layout. Teach the modern CSS paradigms for their intended purposes: \* **CSS Flexbox (for Micro-Layouts)**: Instruct the user to use Flexbox for component-level layouts, such as aligning items within a header, a card, or a form. +- **Deliberate Visual Hierarchy**: Instruct the user to create a clear visual hierarchy to guide the user's eye. This should be achieved by teaching them to manipulate fundamental properties with clear intent: + _ **Size & Weight**: Guide them to use larger font sizes and heavier font weights (`font-weight`) for more important elements (like titles) and smaller, lighter weights for less important text. + _ **Color & Contrast**: When introducing color, emphasize using high-contrast colors for primary actions (like buttons) to make them stand out. +- **Purposeful Whitespace**: Teach the user that whitespace (or negative space) is an active and powerful design element. + _ **Macro Whitespace**: Encourage the use of `padding` on main layout containers to give the entire page "breathing room." + _ **Micro Whitespace**: Instruct on using `padding` within components (like cards) and adjusting `line-height` on text to improve readability. + +### 14. Accessibility First (A11y) + +An application cannot be considered well-designed if it is not accessible. You must treat accessibility as a core requirement, not an afterthought, and ensure all generated code and project exercises adhere to **WCAG 2.2 Level AA** standards. + +- **Mandate Semantic HTML**: Instruct the user to always use semantic HTML elements for their intended purpose (`
+ + +
+
ng generate component xyz
+
ng add @angular/material
+
ng add @angular/pwa
+
ng add _____
+
ng test
+
ng build
+
+ + + + + + + + + Gray Clouds Background + + + + + + + + + + + + + diff --git a/tests/e2e/assets/ssr-project-webpack/src/app/app.component.spec.ts b/tests/e2e/assets/ssr-project-webpack/src/app/app.component.spec.ts new file mode 100644 index 000000000000..c3fc75313bcc --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/app/app.component.spec.ts @@ -0,0 +1,33 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([])], + declarations: [AppComponent], + }), + ); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title '20-ssr-project-webpack'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('20-ssr-project-webpack'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain( + '20-ssr-project-webpack app is running!', + ); + }); +}); diff --git a/tests/e2e/assets/ssr-project-webpack/src/app/app.component.ts b/tests/e2e/assets/ssr-project-webpack/src/app/app.component.ts new file mode 100644 index 000000000000..20b0fef78f45 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/app/app.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + standalone: false, + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], +}) +export class AppComponent { + title = '20-ssr-project-webpack'; +} diff --git a/tests/e2e/assets/ssr-project-webpack/src/app/app.module.server.ts b/tests/e2e/assets/ssr-project-webpack/src/app/app.module.server.ts new file mode 100644 index 000000000000..d182a9f3e994 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/app/app.module.server.ts @@ -0,0 +1,11 @@ +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/tests/e2e/assets/ssr-project-webpack/src/app/app.module.ts b/tests/e2e/assets/ssr-project-webpack/src/app/app.module.ts new file mode 100644 index 000000000000..700cb243fffa --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/app/app.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, AppRoutingModule], + providers: [provideClientHydration()], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/assets/.gitkeep b/tests/e2e/assets/ssr-project-webpack/src/assets/.gitkeep similarity index 100% rename from tests/legacy-cli/e2e/assets/8.0-project/src/assets/.gitkeep rename to tests/e2e/assets/ssr-project-webpack/src/assets/.gitkeep diff --git a/packages/schematics/angular/application/files/src/favicon.ico.template b/tests/e2e/assets/ssr-project-webpack/src/favicon.ico similarity index 100% rename from packages/schematics/angular/application/files/src/favicon.ico.template rename to tests/e2e/assets/ssr-project-webpack/src/favicon.ico diff --git a/tests/e2e/assets/ssr-project-webpack/src/index.html b/tests/e2e/assets/ssr-project-webpack/src/index.html new file mode 100644 index 000000000000..28adeacc85ed --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/index.html @@ -0,0 +1,13 @@ + + + + + 17SsrProjectWebpack + + + + + + + + diff --git a/tests/e2e/assets/ssr-project-webpack/src/main.server.ts b/tests/e2e/assets/ssr-project-webpack/src/main.server.ts new file mode 100644 index 000000000000..dfb6fdb3f1f0 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/main.server.ts @@ -0,0 +1 @@ +export { AppServerModule as default } from './app/app.module.server'; diff --git a/tests/e2e/assets/ssr-project-webpack/src/main.ts b/tests/e2e/assets/ssr-project-webpack/src/main.ts new file mode 100644 index 000000000000..55b91297823b --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/main.ts @@ -0,0 +1,6 @@ +import { platformBrowser } from '@angular/platform-browser'; +import { AppModule } from './app/app.module'; + +platformBrowser() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/tests/e2e/assets/ssr-project-webpack/src/styles.css b/tests/e2e/assets/ssr-project-webpack/src/styles.css new file mode 100644 index 000000000000..90d4ee0072ce --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/tests/e2e/assets/ssr-project-webpack/tsconfig.app.json b/tests/e2e/assets/ssr-project-webpack/tsconfig.app.json new file mode 100644 index 000000000000..84f1f992d275 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/tests/e2e/assets/ssr-project-webpack/tsconfig.json b/tests/e2e/assets/ssr-project-webpack/tsconfig.json new file mode 100644 index 000000000000..bbc051d01524 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/tsconfig.json @@ -0,0 +1,28 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/tests/e2e/assets/ssr-project-webpack/tsconfig.server.json b/tests/e2e/assets/ssr-project-webpack/tsconfig.server.json new file mode 100644 index 000000000000..3b9de71a23f6 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/tsconfig.server.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "./out-tsc/server", + "types": ["node"] + }, + "files": ["src/main.server.ts", "server.ts"] +} diff --git a/tests/e2e/assets/ssr-project-webpack/tsconfig.spec.json b/tests/e2e/assets/ssr-project-webpack/tsconfig.spec.json new file mode 100644 index 000000000000..47e3dd755170 --- /dev/null +++ b/tests/e2e/assets/ssr-project-webpack/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/tests/e2e/initialize/300-log-environment.ts b/tests/e2e/initialize/300-log-environment.ts new file mode 100644 index 000000000000..50b7d752635f --- /dev/null +++ b/tests/e2e/initialize/300-log-environment.ts @@ -0,0 +1,19 @@ +import { getActivePackageManager } from '../utils/packages'; +import { exec, ng } from '../utils/process'; + +export default async function () { + console.log('Environment:'); + + Object.keys(process.env).forEach((envName) => { + // CI Logs should not contain environment variables that are considered secret + const lowerName = envName.toLowerCase(); + if (lowerName.includes('key') || lowerName.includes('secret')) { + return; + } + + console.log(` ${envName}: ${process.env[envName]!.replace(/[\n\r]+/g, '\n ')}`); + }); + + await exec('which', 'ng', getActivePackageManager()); + await ng('version'); +} diff --git a/tests/e2e/initialize/500-create-project.ts b/tests/e2e/initialize/500-create-project.ts new file mode 100644 index 000000000000..b48c8733a9a5 --- /dev/null +++ b/tests/e2e/initialize/500-create-project.ts @@ -0,0 +1,86 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../utils/env'; +import { expectFileToExist } from '../utils/fs'; +import { gitClean } from '../utils/git'; +import { getActivePackageManager, setRegistry as setNPMConfigRegistry } from '../utils/packages'; +import { ng } from '../utils/process'; +import { prepareProjectForE2e, updateJsonFile } from '../utils/project'; + +export default async function () { + const argv: Record = getGlobalVariable('argv'); + + if (argv.noproject) { + return; + } + + if (argv.reuse && typeof argv.reuse === 'string') { + process.chdir(argv.reuse); + await gitClean(); + } else { + // Ensure local test registry is used when outside a project + await setNPMConfigRegistry(true); + + await ng( + 'new', + 'test-project', + '--skip-install', + '--test-runner', + 'karma', + '--package-manager', + getActivePackageManager(), + ); + + await expectFileToExist(join(process.cwd(), 'test-project')); + process.chdir('./test-project'); + + // Setup esbuild builder if requested on the commandline + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + outputPath: 'dist/test-project/browser', + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + buildOptimizer: false, + }; + + const serve = json['projects']['test-project']['architect']['serve']; + serve.builder = '@angular-devkit/build-angular:dev-server'; + + const extract = json['projects']['test-project']['architect']['extract-i18n']; + if (extract) { + extract.builder = '@angular-devkit/build-angular:extract-i18n'; + } + + const test = json['projects']['test-project']['architect']['test']; + test.builder = '@angular-devkit/build-angular:karma'; + test.options ??= {}; + test.options.tsConfig = 'tsconfig.spec.json'; + delete test.options.runner; + }); + await updateJsonFile('tsconfig.json', (tsconfig) => { + delete tsconfig.compilerOptions.esModuleInterop; + tsconfig.compilerOptions.allowSyntheticDefaultImports = true; + }); + } + + // Always need `@angular-devkit/build-angular` due to the use of protractor + await updateJsonFile('package.json', (packageJson) => { + packageJson.devDependencies['@angular-devkit/build-angular'] = + packageJson.devDependencies['@angular/build']; + }); + } + + await prepareProjectForE2e('test-project'); + await ng('version'); +} diff --git a/tests/e2e/initialize/BUILD.bazel b/tests/e2e/initialize/BUILD.bazel new file mode 100644 index 000000000000..2ab5b570925f --- /dev/null +++ b/tests/e2e/initialize/BUILD.bazel @@ -0,0 +1,16 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "initialize", + testonly = True, + srcs = glob(["**/*.ts"]), + data = [ + "//:config-files", + ], + deps = [ + "//:node_modules/@types/node", + "//tests/e2e/utils", + ], +) diff --git a/tests/e2e/ng-snapshot/BUILD.bazel b/tests/e2e/ng-snapshot/BUILD.bazel new file mode 100644 index 000000000000..63d76e1a07da --- /dev/null +++ b/tests/e2e/ng-snapshot/BUILD.bazel @@ -0,0 +1,7 @@ +load("//tools:defaults.bzl", "copy_to_bin") + +copy_to_bin( + name = "ng-snapshot", + srcs = ["package.json"], + visibility = ["//visibility:public"], +) diff --git a/tests/e2e/ng-snapshot/package.json b/tests/e2e/ng-snapshot/package.json new file mode 100644 index 000000000000..a8565caab740 --- /dev/null +++ b/tests/e2e/ng-snapshot/package.json @@ -0,0 +1,22 @@ +{ + "description": "snapshot versions of Angular for e2e testing", + "private": true, + "dependencies": { + "@angular/animations": "github:angular/animations-builds#80524f5d854b7fdb33104e629ca5e1102255f6f5", + "@angular/cdk": "github:angular/cdk-builds#475fd8d473ba20045b3393423d8a14d93a2da938", + "@angular/common": "github:angular/common-builds#8aafef4ce946a2f96a0203c9b9623c29998995bb", + "@angular/compiler": "github:angular/compiler-builds#205d342032c053a41088e14de00ff14e28e56fce", + "@angular/compiler-cli": "github:angular/compiler-cli-builds#110918badaf4f73a2d20eb58f185fb6c1f8ae54a", + "@angular/core": "github:angular/core-builds#969d56512e639d39092dd3edf736bd7ba19a4c7d", + "@angular/forms": "github:angular/forms-builds#fafffa726c829fd644957e6c76bb5e42f09cbed7", + "@angular/language-service": "github:angular/language-service-builds#41e0bb51496678a972f37cb973ece48e40bc5cd1", + "@angular/localize": "github:angular/localize-builds#ef4c1e3562f99602b38312aabccb4cb5fb6bb53f", + "@angular/material": "github:angular/material-builds#998cd492b05bf024e5bb7a9a21e52a94dec740ef", + "@angular/material-moment-adapter": "github:angular/material-moment-adapter-builds#f173ac254a842de20a90694e34628c165989e9b6", + "@angular/platform-browser": "github:angular/platform-browser-builds#90c22fc35fde1feb2bc28435d23062443ddaae68", + "@angular/platform-browser-dynamic": "github:angular/platform-browser-dynamic-builds#fbb14077cfb88dcdb4e5d1b7a177b3d127e9f8be", + "@angular/platform-server": "github:angular/platform-server-builds#0e40e41e36ffd1d1aa740438f11262542da4cd7e", + "@angular/router": "github:angular/router-builds#ac4ab9493afbfbcb3cb457682d651113938d490b", + "@angular/service-worker": "github:angular/service-worker-builds#4ffbe09669ce6958a3ffb669fa7a059aa9e735c6" + } +} diff --git a/tests/e2e/setup/001-npm-sandbox.ts b/tests/e2e/setup/001-npm-sandbox.ts new file mode 100644 index 000000000000..dcd5f8a1a021 --- /dev/null +++ b/tests/e2e/setup/001-npm-sandbox.ts @@ -0,0 +1,55 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { getGlobalVariable, setGlobalVariable } from '../utils/env'; + +/** + * Configure npm to use a unique sandboxed environment. + */ +export default async function () { + const tempRoot: string = getGlobalVariable('tmp-root'); + const npmModulesPrefix = join(tempRoot, 'npm-global'); + const yarnModulesPrefix = join(tempRoot, 'yarn-global'); + const npmRegistry: string = getGlobalVariable('package-registry'); + const npmrc = join(tempRoot, '.npmrc'); + const yarnrc = join(tempRoot, '.yarnrc'); + + // Change the npm+yarn userconfig to the sandboxed npmrc to override the default ~ + process.env.NPM_CONFIG_USERCONFIG = npmrc; + + // The npm+yarn registry URL + process.env.NPM_CONFIG_REGISTRY = npmRegistry; + + // Configure npm+yarn to use a sandboxed bin directory + // From this point onward all yarn/npm bin files/symlinks are put into the prefix directories + process.env.NPM_CONFIG_PREFIX = npmModulesPrefix; + process.env.YARN_CONFIG_PREFIX = yarnModulesPrefix; + + // Put the npm+yarn caches in the temp dir + process.env.NPM_CONFIG_CACHE = join(tempRoot, 'npm-cache'); + process.env.YARN_CACHE_FOLDER = join(tempRoot, 'yarn-cache'); + + // Snapshot builds may contain versions that are not yet released (e.g., RC phase main branch). + // In this case peer dependency ranges may not resolve causing npm 7+ to fail during tests. + // To support this case, legacy peer dependency mode is enabled for snapshot builds. + if (getGlobalVariable('argv')['ng-snapshots']) { + process.env['NPM_CONFIG_legacy_peer_deps'] = 'true'; + } + + // Configure the registry and prefix used within the test sandbox via rc files + await writeFile(npmrc, `registry=${npmRegistry}\nprefix=${npmModulesPrefix}`); + await writeFile(yarnrc, `registry ${npmRegistry}\nprefix ${yarnModulesPrefix}`); + + await mkdir(npmModulesPrefix); + await mkdir(yarnModulesPrefix); + + setGlobalVariable('npm-global', npmModulesPrefix); + setGlobalVariable('yarn-global', yarnModulesPrefix); + + // Disable all update/notification related npm/yarn features such as the NPM updater notifier. + // The NPM updater notifier may prevent the child process from closing until it timeouts after 3 minutes. + process.env.NO_UPDATE_NOTIFIER = '1'; + process.env.NPM_CONFIG_UPDATE_NOTIFIER = 'false'; + + console.log(` Using "${npmModulesPrefix}" as e2e test global npm bin dir.`); + console.log(` Using "${yarnModulesPrefix}" as e2e test global yarn bin dir.`); +} diff --git a/tests/e2e/setup/010-local-publish.ts b/tests/e2e/setup/010-local-publish.ts new file mode 100644 index 000000000000..b58a5b871daf --- /dev/null +++ b/tests/e2e/setup/010-local-publish.ts @@ -0,0 +1,29 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path/posix'; +import { getGlobalVariable } from '../utils/env'; +import { PkgInfo } from '../utils/packages'; +import { globalNpm, extractNpmEnv, extractCIAndInfraEnv } from '../utils/process'; +import { isPrereleaseCli } from '../utils/project'; + +export default async function () { + const testRegistry: string = getGlobalVariable('package-registry'); + const packageTars: PkgInfo[] = Object.values(getGlobalVariable('package-tars')); + const npmrc = join(getGlobalVariable('tmp-root'), '.npmrc-publish'); + await writeFile( + npmrc, + ` + ${testRegistry.replace(/^https?:/, '')}/:_authToken=fake-secret + `, + ); + + // Publish packages specified with --package + await Promise.all( + packageTars.map(({ path: p }) => + globalNpm(['publish', '--tag', isPrereleaseCli() ? 'next' : 'latest', p], { + ...extractNpmEnv(), + ...extractCIAndInfraEnv(), + 'NPM_CONFIG_USERCONFIG': npmrc, + }), + ), + ); +} diff --git a/tests/e2e/setup/100-global-cli.ts b/tests/e2e/setup/100-global-cli.ts new file mode 100644 index 000000000000..9f587fa5c38d --- /dev/null +++ b/tests/e2e/setup/100-global-cli.ts @@ -0,0 +1,29 @@ +import { getGlobalVariable } from '../utils/env'; +import { getActivePackageManager } from '../utils/packages'; +import { globalNpm } from '../utils/process'; + +const PACKAGE_MANAGER_VERSION = { + 'npm': '10.8.1', + 'yarn': '1.22.22', + 'pnpm': '10.17.1', + 'bun': '1.3.2', +}; + +export default async function () { + const argv = getGlobalVariable('argv'); + if (argv.noglobal) { + return; + } + + const testRegistry = getGlobalVariable('package-registry'); + const packageManager = getActivePackageManager(); + + // Install global Angular CLI being tested, npm+yarn used by e2e tests. + await globalNpm([ + 'install', + '--global', + `--registry=${testRegistry}`, + '@angular/cli', + `${packageManager}@${PACKAGE_MANAGER_VERSION[packageManager]}`, + ]); +} diff --git a/tests/e2e/setup/200-create-project-dir.ts b/tests/e2e/setup/200-create-project-dir.ts new file mode 100644 index 000000000000..1e1e049956bf --- /dev/null +++ b/tests/e2e/setup/200-create-project-dir.ts @@ -0,0 +1,19 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import { getGlobalVariable, setGlobalVariable } from '../utils/env'; + +/** + * Create a parent directory for test projects to be created within. + * Change the cwd() to that directory in preparation for launching the cli. + */ +export default async function () { + const tempRoot: string = getGlobalVariable('tmp-root'); + const projectsRoot = join(tempRoot, 'e2e-test'); + + setGlobalVariable('projects-root', projectsRoot); + + await mkdir(projectsRoot); + + console.log(` Using "${projectsRoot}" as temporary directory for a new project.`); + process.chdir(projectsRoot); +} diff --git a/tests/e2e/setup/BUILD.bazel b/tests/e2e/setup/BUILD.bazel new file mode 100644 index 000000000000..36fe39fa3409 --- /dev/null +++ b/tests/e2e/setup/BUILD.bazel @@ -0,0 +1,13 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "setup", + testonly = True, + srcs = glob(["**/*.ts"]), + deps = [ + "//:node_modules/@types/node", + "//tests/e2e/utils", + ], +) diff --git a/tests/e2e/tests/BUILD.bazel b/tests/e2e/tests/BUILD.bazel new file mode 100644 index 000000000000..891814cb24eb --- /dev/null +++ b/tests/e2e/tests/BUILD.bazel @@ -0,0 +1,20 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "tests", + testonly = True, + srcs = glob(["**/*.ts"]), + deps = [ + "//:node_modules/@types/express", + "//:node_modules/@types/node", + "//:node_modules/@types/semver", + "//:node_modules/express", + "//:node_modules/fast-glob", + "//:node_modules/puppeteer", + "//:node_modules/semver", + "//:node_modules/undici", + "//tests/e2e/utils", + ], +) diff --git a/tests/e2e/tests/architect_cli/direct_execution.ts b/tests/e2e/tests/architect_cli/direct_execution.ts new file mode 100644 index 000000000000..b91010d46283 --- /dev/null +++ b/tests/e2e/tests/architect_cli/direct_execution.ts @@ -0,0 +1,14 @@ +import * as assert from 'node:assert/strict'; +import { exec } from '../../utils/process'; +import { join } from 'node:path'; + +export default async function () { + // Run help command + const binPath = join('node_modules', '.bin', 'architect'); + const { stdout } = await exec(binPath, '--help'); + + assert.ok( + stdout.includes('architect [project][:target][:configuration] [options, ...]'), + 'Expected stdout to contain usage information.', + ); +} diff --git a/tests/e2e/tests/architect_cli/package_execution.ts b/tests/e2e/tests/architect_cli/package_execution.ts new file mode 100644 index 000000000000..60b3964b7521 --- /dev/null +++ b/tests/e2e/tests/architect_cli/package_execution.ts @@ -0,0 +1,22 @@ +import * as assert from 'node:assert/strict'; +import { exec } from '../../utils/process'; +import { installPackage, uninstallPackage } from '../../utils/packages'; +import { join } from 'node:path'; + +export default async function () { + // Install CLI package + await installPackage('@angular-devkit/architect-cli'); + + try { + // Run help command + const binPath = join('node_modules', '.bin', 'architect'); + const { stdout } = await exec(binPath, '--help'); + + assert.ok( + stdout.includes('architect [project][:target][:configuration] [options, ...]'), + 'Expected stdout to contain usage information.', + ); + } finally { + await uninstallPackage('@angular-devkit/architect-cli'); + } +} diff --git a/tests/e2e/tests/basic/aot.ts b/tests/e2e/tests/basic/aot.ts new file mode 100644 index 000000000000..d462d817332a --- /dev/null +++ b/tests/e2e/tests/basic/aot.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { ng } from '../../utils/process'; + +/** + * AOT builds should contain generated component factories + */ +export default async function () { + await ng('build', '--aot=true', '--configuration=development'); + const content = await readFile('dist/test-project/browser/main.js', 'utf-8'); + assert.match(content, /App_Factory/); +} diff --git a/tests/e2e/tests/basic/build.ts b/tests/e2e/tests/basic/build.ts new file mode 100644 index 000000000000..2026b3988d2b --- /dev/null +++ b/tests/e2e/tests/basic/build.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; + +const OUTPUT_INDEX_PATH = 'dist/test-project/browser/index.html'; + +export default async function () { + // Development build + const { stdout: stdoutDev } = await ng('build', '--configuration=development'); + // Console output should not contain estimated transfer size information + assert.doesNotMatch(stdoutDev, /Estimated transfer size/); + // Output index HTML file should reference main JS file + const devIndexContent = await readFile(OUTPUT_INDEX_PATH, 'utf-8'); + assert.match(devIndexContent, /main\.js/); + + const usingApplicationBuilder = getGlobalVariable('argv')['esbuild']; + + // Production build + const { stdout: stdoutProd } = await ng('build'); + // Console output should contain estimated transfer size information + assert.match(stdoutProd, /Estimated transfer size/); + // Output index HTML file should reference main JS file with hashing + const prodIndexContent = await readFile(OUTPUT_INDEX_PATH, 'utf-8'); + + if (usingApplicationBuilder) { + // application builder uses an 8 character hash and a dash as a separator + assert.match(prodIndexContent, /main-[a-zA-Z0-9]{8}\.js/); + } else { + // browser builder uses a 16 character hash and a period as a separator + assert.match(prodIndexContent, /main\.[a-zA-Z0-9]{16}\.js/); + } +} diff --git a/tests/e2e/tests/basic/command-scope.ts b/tests/e2e/tests/basic/command-scope.ts new file mode 100644 index 000000000000..f83ad21112b4 --- /dev/null +++ b/tests/e2e/tests/basic/command-scope.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { homedir } from 'node:os'; +import { silentNg } from '../../utils/process'; + +export default async function () { + // Run inside workspace + await silentNg('generate', 'component', 'foo', '--dry-run'); + + // The version command can be run in and outside of a workspace. + await silentNg('version'); + + await assert.rejects( + silentNg('new', 'proj-name', '--dry-run'), + /This command is not available when running the Angular CLI inside a workspace\./, + ); + + // Change CWD to run outside a workspace. + process.chdir(homedir()); + + // ng generate can only be ran inside. + await assert.rejects( + silentNg('generate', 'component', 'foo', '--dry-run'), + /This command is not available when running the Angular CLI outside a workspace\./, + ); + + // ng new can only be ran outside of a workspace + await silentNg('new', 'proj-name', '--dry-run'); + + // The version command can be run in and outside of a workspace. + await silentNg('version'); +} diff --git a/tests/e2e/tests/basic/rebuild.ts b/tests/e2e/tests/basic/rebuild.ts new file mode 100644 index 000000000000..a0b0f1ddc79d --- /dev/null +++ b/tests/e2e/tests/basic/rebuild.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import { setTimeout } from 'node:timers/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, replaceInFile, writeMultipleFiles } from '../../utils/fs'; +import { silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { ngServe } from '../../utils/project'; + +export default async function () { + const esbuild = getGlobalVariable('argv')['esbuild']; + const validBundleRegEx = esbuild ? /sent to client/ : /Compiled successfully\./; + const lazyBundleRegEx = esbuild ? /chunk-/ : /src_app_lazy_lazy_ts\.js/; + + // Disable HMR to support page reload based rebuild testing. + const port = await ngServe('--no-hmr'); + + // Add a lazy route. + await silentNg('generate', 'component', 'lazy'); + + // Should trigger a rebuild with a new bundle. + // We need to use Promise.all to ensure we are waiting for the rebuild just before we write + // the file, otherwise rebuilds can be too fast and fail CI. + // Count the bundles. + // Verify that a new chunk was created. + await Promise.all([ + waitForAnyProcessOutputToMatch(lazyBundleRegEx), + replaceInFile( + 'src/app/app.routes.ts', + 'routes: Routes = [];', + `routes: Routes = [{path: 'lazy', loadComponent: () => import('./lazy/lazy').then(c => c.Lazy)}];`, + ), + ]); + + // Change multiple files and check that all of them are invalidated and recompiled. + await setTimeout(500); + await Promise.all([ + waitForAnyProcessOutputToMatch(validBundleRegEx), + appendToFile( + 'src/app/app.routes.ts', + ` + console.log('$$_E2E_GOLDEN_VALUE_1'); + export let X = '$$_E2E_GOLDEN_VALUE_2'; + `, + ), + appendToFile( + 'src/main.ts', + ` + import * as m from './app/app.routes'; + console.log(m.X); + console.log('$$_E2E_GOLDEN_VALUE_3'); + `, + ), + ]); + + await setTimeout(500); + await Promise.all([ + waitForAnyProcessOutputToMatch(validBundleRegEx), + writeMultipleFiles({ + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + + export const routes: Routes = []; + + console.log('$$_E2E_GOLDEN_VALUE_1'); + export let X = '$$_E2E_GOLDEN_VALUE_2'; + console.log('File changed with no import/export changes'); + `, + }), + ]); + { + const response = await fetch(`http://localhost:${port}/main.js`); + const body = await response.text(); + assert.match(body, /\$\$_E2E_GOLDEN_VALUE_1/); + assert.match(body, /\$\$_E2E_GOLDEN_VALUE_2/); + assert.match(body, /\$\$_E2E_GOLDEN_VALUE_3/); + } + + await setTimeout(500); + await Promise.all([ + waitForAnyProcessOutputToMatch(validBundleRegEx), + writeMultipleFiles({ + 'src/app/app.html': '

testingTESTING123

', + }), + ]); + + { + const response = await fetch(`http://localhost:${port}/main.js`); + const body = await response.text(); + assert.match(body, /testingTESTING123/); + } + + await setTimeout(500); + await Promise.all([ + waitForAnyProcessOutputToMatch(validBundleRegEx), + writeMultipleFiles({ + 'src/app/app.css': ':host { color: blue; }', + }), + ]); + + { + const response = await fetch(`http://localhost:${port}/main.js`); + const body = await response.text(); + assert.match(body, /color:\s?blue/); + } + + await setTimeout(500); + await Promise.all([ + waitForAnyProcessOutputToMatch(validBundleRegEx), + writeMultipleFiles({ + 'src/styles.css': 'div { color: green; }', + }), + ]); + + { + const response = await fetch(`http://localhost:${port}/styles.css`); + const body = await response.text(); + assert.match(body, /color:\s?green/); + } +} diff --git a/tests/e2e/tests/basic/run.ts b/tests/e2e/tests/basic/run.ts new file mode 100644 index 000000000000..c5986a8926a4 --- /dev/null +++ b/tests/e2e/tests/basic/run.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { silentNg } from '../../utils/process'; + +const OUTPUT_INDEX_PATH = 'dist/test-project/browser/index.html'; + +export default async function () { + // Development build + await silentNg('run', 'test-project:build:development'); + // Output index HTML file should reference main JS file + const devIndexContent = await readFile(OUTPUT_INDEX_PATH, 'utf-8'); + assert.match(devIndexContent, /main\.js/); + + const usingApplicationBuilder = getGlobalVariable('argv')['esbuild']; + + // Production build + await silentNg('run', 'test-project:build'); + // Output index HTML file should reference main JS file with hashing + const prodIndexContent = await readFile(OUTPUT_INDEX_PATH, 'utf-8'); + if (usingApplicationBuilder) { + // application builder uses an 8 character hash and a dash as a separator + assert.match(prodIndexContent, /main-[a-zA-Z0-9]{8}\.js/); + } else { + // browser builder uses a 16 character hash and a period as a separator + assert.match(prodIndexContent, /main\.[a-zA-Z0-9]{16}\.js/); + } +} diff --git a/tests/e2e/tests/basic/scripts-array.ts b/tests/e2e/tests/basic/scripts-array.ts new file mode 100644 index 000000000000..0721b120da2b --- /dev/null +++ b/tests/e2e/tests/basic/scripts-array.ts @@ -0,0 +1,78 @@ +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + await writeMultipleFiles({ + 'src/string-script.js': "console.log('string-script'); var number = 1+1;", + 'src/zstring-script.js': "console.log('zstring-script');", + 'src/fstring-script.js': "console.log('fstring-script');", + 'src/ustring-script.js': "console.log('ustring-script');", + 'src/bstring-script.js': "console.log('bstring-script');", + 'src/astring-script.js': "console.log('astring-script');", + 'src/cstring-script.js': "console.log('cstring-script');", + 'src/input-script.js': "console.log('input-script');", + 'src/lazy-script.js': "console.log('lazy-script');", + 'src/pre-rename-script.js': "console.log('pre-rename-script');", + 'src/pre-rename-lazy-script.js': "console.log('pre-rename-lazy-script');", + }); + + await appendToFile('src/main.ts', "import './string-script.js';"); + + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + appArchitect.build.options.scripts = [ + { input: 'src/string-script.js' }, + { input: 'src/zstring-script.js' }, + { input: 'src/fstring-script.js' }, + { input: 'src/ustring-script.js' }, + { input: 'src/bstring-script.js' }, + { input: 'src/astring-script.js' }, + { input: 'src/cstring-script.js' }, + { input: 'src/input-script.js' }, + { input: 'src/lazy-script.js', inject: false }, + { input: 'src/pre-rename-script.js', bundleName: 'renamed-script' }, + { + input: 'src/pre-rename-lazy-script.js', + bundleName: 'renamed-lazy-script', + inject: false, + }, + ]; + }); + + await ng('build', '--configuration=development'); + + // files were created successfully + await expectFileToMatch('dist/test-project/browser/scripts.js', 'string-script'); + await expectFileToMatch('dist/test-project/browser/scripts.js', 'input-script'); + await expectFileToMatch('dist/test-project/browser/lazy-script.js', 'lazy-script'); + await expectFileToMatch('dist/test-project/browser/renamed-script.js', 'pre-rename-script'); + await expectFileToMatch( + 'dist/test-project/browser/renamed-lazy-script.js', + 'pre-rename-lazy-script', + ); + + // index.html lists the right bundles + if (getGlobalVariable('argv')['esbuild']) { + await expectFileToMatch( + 'dist/test-project/browser/index.html', + [ + '', + '', + '', + ].join(''), + ); + } else { + await expectFileToMatch( + 'dist/test-project/browser/index.html', + [ + '', + '', + '', + '', + '', + ].join(''), + ); + } +} diff --git a/tests/e2e/tests/basic/serve.ts b/tests/e2e/tests/basic/serve.ts new file mode 100644 index 000000000000..eac4823a3126 --- /dev/null +++ b/tests/e2e/tests/basic/serve.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { killAllProcesses } from '../../utils/process'; +import { ngServe } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + // Serve works without HMR + const noHmrPort = await ngServe('--no-hmr'); + await verifyResponse(noHmrPort); + await killAllProcesses(); + + // Serve works with HMR + const hmrPort = await ngServe('--hmr'); + await verifyResponse(hmrPort); + + await executeBrowserTest({ baseUrl: `http://localhost:${hmrPort}/` }); +} + +async function verifyResponse(port: number): Promise { + const indexResponse = await fetch(`http://localhost:${port}/`); + assert.match(await indexResponse.text(), /<\/app-root>/); + + const assetResponse = await fetch(`http://localhost:${port}/favicon.ico`); + assert(assetResponse.ok, 'Expected favicon asset to be available.'); +} diff --git a/tests/e2e/tests/basic/styles-array.ts b/tests/e2e/tests/basic/styles-array.ts new file mode 100644 index 000000000000..cc4d6d56f506 --- /dev/null +++ b/tests/e2e/tests/basic/styles-array.ts @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import { expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + await writeMultipleFiles({ + 'src/string-style.css': '.string-style { color: red }', + 'src/input-style.css': '.input-style { color: red }', + 'src/lazy-style.css': '.lazy-style { color: red }', + 'src/pre-rename-style.css': '.pre-rename-style { color: red }', + 'src/pre-rename-lazy-style.css': '.pre-rename-lazy-style { color: red }', + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [ + { input: 'src/string-style.css' }, + { input: 'src/input-style.css' }, + { input: 'src/lazy-style.css', inject: false }, + { input: 'src/pre-rename-style.css', bundleName: 'renamed-style' }, + { + input: 'src/pre-rename-lazy-style.css', + bundleName: 'renamed-lazy-style', + inject: false, + }, + ]; + }); + + const { stdout } = await ng('build', '--configuration=development'); + + await expectFileToMatch('dist/test-project/browser/styles.css', '.string-style'); + await expectFileToMatch('dist/test-project/browser/styles.css', '.input-style'); + await expectFileToMatch('dist/test-project/browser/lazy-style.css', '.lazy-style'); + await expectFileToMatch('dist/test-project/browser/renamed-style.css', '.pre-rename-style'); + await expectFileToMatch( + 'dist/test-project/browser/renamed-lazy-style.css', + '.pre-rename-lazy-style', + ); + await expectFileToMatch( + 'dist/test-project/browser/index.html', + '', + ); + + // Non injected styles should be listed under lazy chunk files + assert.match(stdout, /Lazy chunk files[\s\S]+renamed-lazy-style\.css/m); +} diff --git a/tests/e2e/tests/basic/test.ts b/tests/e2e/tests/basic/test.ts new file mode 100644 index 000000000000..50580581d442 --- /dev/null +++ b/tests/e2e/tests/basic/test.ts @@ -0,0 +1,59 @@ +import { ng } from '../../utils/process'; +import { writeMultipleFiles } from '../../utils/fs'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + // make sure both --watch=false work + await ng('test', '--watch=false'); + + // Works with custom config + await writeMultipleFiles({ + './karma.conf.bis.js': ` + // Karma configuration file, see link for more information + // https://karma-runner.github.io/1.0/config/configuration-file.html + 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 + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: [ + '--no-sandbox', + '--headless', + '--disable-gpu', + '--disable-dev-shm-usage', + ], + } + }, + singleRun: false, + restartOnFileChange: true + }); + }; + `, + }); + + const isWebpack = !getGlobalVariable('argv')['esbuild']; + + if (isWebpack) { + await ng('test', '--watch=false', '--karma-config=karma.conf.bis.js'); + } else { + await ng('test', '--watch=false', '--runner-config=karma.conf.bis.js'); + } +} diff --git a/tests/e2e/tests/build/app-shell/app-shell-ngmodule.ts b/tests/e2e/tests/build/app-shell/app-shell-ngmodule.ts new file mode 100644 index 000000000000..b1b6cfab499d --- /dev/null +++ b/tests/e2e/tests/build/app-shell/app-shell-ngmodule.ts @@ -0,0 +1,35 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToMatch } from '../../../utils/fs'; +import { installPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +const snapshots = require('../../../ng-snapshot/package.json'); + +export default async function () { + await ng('generate', 'app', 'test-project-two', '--routing', '--no-standalone', '--skip-install'); + await ng('generate', 'app-shell', '--project', 'test-project-two'); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + const packagesToInstall: string[] = []; + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries( + snapshots.dependencies as { [p: string]: string }, + )) { + if (name in dependencies && dependencies[name] !== version) { + packagesToInstall.push(version); + } + } + }); + + for (const pkg of packagesToInstall) { + await installPackage(pkg); + } + } + + await ng('build', 'test-project-two'); + await expectFileToMatch('dist/test-project-two/browser/index.html', 'app-shell works!'); +} diff --git a/tests/e2e/tests/build/app-shell/app-shell-with-schematic.ts b/tests/e2e/tests/build/app-shell/app-shell-with-schematic.ts new file mode 100644 index 000000000000..65f6fdb23c48 --- /dev/null +++ b/tests/e2e/tests/build/app-shell/app-shell-with-schematic.ts @@ -0,0 +1,34 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { appendToFile, expectFileToMatch } from '../../../utils/fs'; +import { installPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +const snapshots = require('../../../ng-snapshot/package.json'); + +export default async function () { + await appendToFile('src/app/app.html', ''); + await ng('generate', 'app-shell', '--project', 'test-project'); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + const packagesToInstall: string[] = []; + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries( + snapshots.dependencies as { [p: string]: string }, + )) { + if (name in dependencies && dependencies[name] !== version) { + packagesToInstall.push(version); + } + } + }); + + for (const pkg of packagesToInstall) { + await installPackage(pkg); + } + } + await ng('build'); + await expectFileToMatch('dist/test-project/browser/index.html', 'app-shell works!'); +} diff --git a/tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts b/tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts new file mode 100644 index 000000000000..b69e28e9ea38 --- /dev/null +++ b/tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts @@ -0,0 +1,56 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { appendToFile, expectFileToMatch, writeFile } from '../../../utils/fs'; +import { installPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +const snapshots = require('../../../ng-snapshot/package.json'); + +export default async function () { + await appendToFile('src/app/app.html', ''); + await ng('generate', 'service-worker', '--project', 'test-project'); + await ng('generate', 'app-shell', '--project', 'test-project'); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + const packagesToInstall: string[] = []; + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries( + snapshots.dependencies as { [p: string]: string }, + )) { + if (name in dependencies && dependencies[name] !== version) { + packagesToInstall.push(version); + } + } + }); + + for (const pkg of packagesToInstall) { + await installPackage(pkg); + } + } + + await writeFile( + 'e2e/app.e2e-spec.ts', + ` + import { browser, by, element } from 'protractor'; + + it('should have ngsw in normal state', () => { + browser.get('/'); + // Wait for service worker to load. + browser.sleep(2000); + browser.waitForAngularEnabled(false); + browser.get('/ngsw/state'); + // Should have updated, and be in normal state. + expect(element(by.css('pre')).getText()).not.toContain('Last update check: never'); + expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL'); + }); + `, + ); + + await ng('build'); + await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); + + await ng('e2e', '--configuration=production'); +} diff --git a/tests/e2e/tests/build/assets.ts b/tests/e2e/tests/build/assets.ts new file mode 100644 index 000000000000..93c89b5cad86 --- /dev/null +++ b/tests/e2e/tests/build/assets.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { writeFile, stat, mkdir, symlink, utimes } from 'node:fs/promises'; +import { expectFileToExist, expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; +import { getGlobalVariable } from '../../utils/env'; + +const isNodeV22orHigher = Number(process.versions.node.split('.', 1)[0]) >= 22; + +export default async function () { + // Update the atime and mtime of the original file. + // Note: Node.js has different time precision, which may cause mtime-based tests to fail. + // Ensure both values are rounded to the same precision for consistency. + // Example: + // Original: '1742973507738.0234' + // Node.js CP: '1742973507737.999' + const { atime, mtime } = await stat('public/favicon.ico'); + await utimes('public/favicon.ico', atime, mtime); + + await writeFile('public/.file', ''); + await writeFile('public/test.abc', 'hello world'); + + await ng('build', '--configuration=development'); + await expectFileToExist('dist/test-project/browser/favicon.ico'); + await expectFileToExist('dist/test-project/browser/.file'); + await expectFileToMatch('dist/test-project/browser/test.abc', 'hello world'); + await expectToFail(() => expectFileToExist('dist/test-project/browser/.gitkeep')); + + // Timestamp preservation only supported with application build system on Node.js v22+ + if (isNodeV22orHigher && getGlobalVariable('argv')['esbuild']) { + const [originalStats, outputStats] = await Promise.all([ + stat('public/favicon.ico'), + stat('dist/test-project/browser/favicon.ico'), + ]); + + assert.equal( + originalStats.mtimeMs, + outputStats.mtimeMs, + 'Asset file modified timestamp should be preserved.', + ); + } + + // Ensure `followSymlinks` option follows symlinks + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect['build'].options.assets = [ + { glob: '**/*', input: 'public', followSymlinks: true }, + ]; + }); + + await mkdir('dirToSymlink/subdir1', { recursive: true }); + await mkdir('dirToSymlink/subdir2/subsubdir1', { recursive: true }); + await symlink(process.cwd() + '/dirToSymlink', 'public/symlinkDir'); + + await Promise.all([ + writeFile('dirToSymlink/a.txt', ''), + writeFile('dirToSymlink/subdir1/b.txt', ''), + writeFile('dirToSymlink/subdir2/c.txt', ''), + writeFile('dirToSymlink/subdir2/subsubdir1/d.txt', ''), + ]); + + await ng('build', '--configuration=development'); + + await expectFileToExist('dist/test-project/browser/symlinkDir/a.txt'); + await expectFileToExist('dist/test-project/browser/symlinkDir/subdir1/b.txt'); + await expectFileToExist('dist/test-project/browser/symlinkDir/subdir2/c.txt'); + await expectFileToExist('dist/test-project/browser/symlinkDir/subdir2/subsubdir1/d.txt'); +} diff --git a/tests/e2e/tests/build/auto-csp.ts b/tests/e2e/tests/build/auto-csp.ts new file mode 100644 index 000000000000..1839b160d549 --- /dev/null +++ b/tests/e2e/tests/build/auto-csp.ts @@ -0,0 +1,143 @@ +import assert from 'node:assert'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch, writeFile, writeMultipleFiles } from '../../utils/fs'; +import { findFreePort } from '../../utils/network'; +import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +const CSP_META_TAG = / { + const build = json['projects']['test-project']['architect']['build']; + build.options = { + ...build.options, + security: { autoCsp: true }, + }; + }); + + await writeMultipleFiles({ + 'serve.js': ` + const express = require('express'); + const path = require('path'); + + const app = express(); + const PORT = process.env.PORT || 3000; + + app.use(express.static(path.join(__dirname, 'dist/test-project/browser'))); + + app.listen(PORT, () => { + console.log('Node Express server listening on ' + PORT); + }); + `, + 'public/script1.js': ` + const externalScriptCreated = 1337; + console.warn('First External Script: ' + inlineScriptBodyCreated); + `, + 'public/script2.js': `console.warn('Second External Script: ' + externalScriptCreated);`, + 'src/index.html': ` + + + + + + + + + + + + + + + + `, + 'e2e/src/app.e2e-spec.ts': ` + import { browser, by, element } from 'protractor'; + import * as webdriver from 'selenium-webdriver'; + + function allConsoleWarnMessagesAndErrors() { + return browser + .manage() + .logs() + .get('browser') + .then(function (browserLog: any[]) { + const warnMessages: any[] = []; + browserLog.filter((logEntry) => { + const msg = logEntry.message; + console.log('>> ' + msg); + if (logEntry.level.value >= webdriver.logging.Level.INFO.value) { + warnMessages.push(msg); + } + }); + return warnMessages; + }); + } + + describe('Hello world E2E Tests', () => { + beforeAll(async () => { + await browser.waitForAngularEnabled(true); + }); + + it('should display: Welcome and run all scripts in order', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Make sure all scripts ran and there were no client side errors. + const consoleMessages = await allConsoleWarnMessagesAndErrors(); + expect(consoleMessages.length).toEqual(4); // No additional errors + // Extract just the printed messages from the console data. + const printedMessages = consoleMessages.map(m => m.match(/"(.*?)"/)[1]); + expect(printedMessages).toEqual([ + // All messages printed in order because execution order is preserved. + "Inline Script Head", + "Inline Script Body: 1339", + "First External Script: 1338", + "Second External Script: 1337", + ]); + }); + }); + `, + }); + + async function spawnServer(): Promise { + const port = await findFreePort(); + + await execAndWaitForOutputToMatch('node', ['serve.js'], /Node Express server listening on/, { + ...process.env, + 'PORT': String(port), + }); + + return port; + } + + await ng('build'); + + // Make sure the output files have auto-CSP as a result of `ng build` + await expectFileToMatch('dist/test-project/browser/index.html', CSP_META_TAG); + + // Make sure if contains the critical CSS inlining CSP code. + await expectFileToMatch('dist/test-project/browser/index.html', 'ngCspMedia'); + + // Make sure that our e2e protractor tests run to confirm that our angular project runs. + const port = await spawnServer(); + await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); +} diff --git a/tests/e2e/tests/build/bundle-budgets.ts b/tests/e2e/tests/build/bundle-budgets.ts new file mode 100644 index 000000000000..1401d8d3c6e3 --- /dev/null +++ b/tests/e2e/tests/build/bundle-budgets.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 assert from 'node:assert/strict'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + // Error + await updateJsonFile('angular.json', (json) => { + json.projects['test-project'].architect.build.configurations.production.budgets = [ + { type: 'all', maximumError: '100b' }, + ]; + }); + + const { message: errorMessage } = await expectToFail(() => ng('build')); + assert.match(errorMessage, /Error.+budget/i, 'Budget error: all, max error.'); + + // Warning + await updateJsonFile('angular.json', (json) => { + json.projects['test-project'].architect.build.configurations.production.budgets = [ + { type: 'all', minimumWarning: '100mb' }, + ]; + }); + + const { stderr } = await ng('build'); + assert.match(stderr, /Warning.+budget/i, 'Budget warning: all, min warning'); + + // Pass + await updateJsonFile('angular.json', (json) => { + json.projects['test-project'].architect.build.configurations.production.budgets = [ + { type: 'allScript', maximumError: '100mb' }, + ]; + }); + + const { stderr: stderr2 } = await ng('build'); + assert.doesNotMatch(stderr2, /(Warning|Error)/i, 'BIG max for all, should not error'); +} diff --git a/tests/e2e/tests/build/chunk-optimizer-lazy.ts b/tests/e2e/tests/build/chunk-optimizer-lazy.ts new file mode 100644 index 000000000000..7f57e6d88e68 --- /dev/null +++ b/tests/e2e/tests/build/chunk-optimizer-lazy.ts @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { readdir } from 'node:fs/promises'; +import { replaceInFile } from '../../utils/fs'; +import { execWithEnv, ng } from '../../utils/process'; + +export default async function () { + // Add lazy routes. + await ng('generate', 'component', 'lazy-a'); + await ng('generate', 'component', 'lazy-b'); + await ng('generate', 'component', 'lazy-c'); + await replaceInFile( + 'src/app/app.routes.ts', + 'routes: Routes = [];', + `routes: Routes = [ + { + path: 'lazy-a', + loadComponent: () => import('./lazy-a/lazy-a').then(m => m.LazyA), + }, + { + path: 'lazy-b', + loadComponent: () => import('./lazy-b/lazy-b').then(m => m.LazyB), + }, + { + path: 'lazy-c', + loadComponent: () => import('./lazy-c/lazy-c').then(m => m.LazyC), + }, + ];`, + ); + + // Build without chunk optimization + await ng('build', '--output-hashing=none'); + const unoptimizedFiles = await readdir('dist/test-project/browser'); + const unoptimizedJsFiles = unoptimizedFiles.filter((f) => f.endsWith('.js')); + + // Build with chunk optimization + await execWithEnv('ng', ['build', '--output-hashing=none'], { + ...process.env, + NG_BUILD_OPTIMIZE_CHUNKS: '1', + }); + const optimizedFiles = await readdir('dist/test-project/browser'); + const optimizedJsFiles = optimizedFiles.filter((f) => f.endsWith('.js')); + + // Check that the number of chunks is reduced but not all combined + assert.ok( + optimizedJsFiles.length < unoptimizedJsFiles.length, + `Expected chunk count to be less than ${unoptimizedJsFiles.length}, but was ${optimizedJsFiles.length}.`, + ); + assert.ok( + optimizedJsFiles.length > 1, + `Expected more than one chunk, but found ${optimizedJsFiles.length}.`, + ); +} diff --git a/tests/e2e/tests/build/chunk-optimizer.ts b/tests/e2e/tests/build/chunk-optimizer.ts new file mode 100644 index 000000000000..366eaa7b4f3d --- /dev/null +++ b/tests/e2e/tests/build/chunk-optimizer.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { execWithEnv } from '../../utils/process'; + +/** + * AOT builds with chunk optimizer should contain generated component definitions. + * This is currently testing that the generated code is propagating through the + * chunk optimization step. + */ +export default async function () { + await execWithEnv('ng', ['build', '--output-hashing=none'], { + ...process.env, + NG_BUILD_OPTIMIZE_CHUNKS: '1', + NG_BUILD_MANGLE: '0', + }); + + const content = await readFile('dist/test-project/browser/main.js', 'utf-8'); + assert.match(content, /ɵɵdefineComponent/u); +} diff --git a/tests/e2e/tests/build/config-file-fallback.ts b/tests/e2e/tests/build/config-file-fallback.ts new file mode 100644 index 000000000000..53298b573e12 --- /dev/null +++ b/tests/e2e/tests/build/config-file-fallback.ts @@ -0,0 +1,9 @@ +import { ng } from '../../utils/process'; +import { moveFile } from '../../utils/fs'; + +export default function () { + return Promise.resolve() + .then(() => ng('build')) + .then(() => moveFile('angular.json', '.angular.json')) + .then(() => ng('build')); +} diff --git a/tests/e2e/tests/build/css-urls.ts b/tests/e2e/tests/build/css-urls.ts new file mode 100644 index 000000000000..839ecf58f567 --- /dev/null +++ b/tests/e2e/tests/build/css-urls.ts @@ -0,0 +1,225 @@ +import { ng } from '../../utils/process'; +import { + expectFileToMatch, + expectFileToExist, + expectFileMatchToExist, + writeMultipleFiles, +} from '../../utils/fs'; +import { copyProjectAsset } from '../../utils/assets'; +import { expectToFail } from '../../utils/utils'; +import { getGlobalVariable } from '../../utils/env'; +import { mkdir } from 'node:fs/promises'; + +const imgSvg = ` + + + +`; + +export default async function () { + const usingWebpack = !getGlobalVariable('argv')['esbuild']; + + const mediaPath = usingWebpack + ? './dist/test-project/browser' + : './dist/test-project/browser/media'; + + await mkdir('public/assets/', { recursive: true }); + + await Promise.resolve() + // Verify absolute/relative paths in global/component css. + .then(() => + writeMultipleFiles({ + 'src/styles.css': ` + h1 { background: url('/assets/global-img-absolute.svg'); } + h2 { background: url('./assets/global-img-relative.png'); } + `, + 'src/app/app.css': ` + h3 { background: url('/assets/component-img-absolute.svg'); } + h4 { background: url('../assets/component-img-relative.png'); } + `, + 'public/assets/global-img-absolute.svg': imgSvg, + 'public/assets/component-img-absolute.svg': imgSvg, + }), + ) + .then(() => copyProjectAsset('images/spectrum.png', './src/assets/global-img-relative.png')) + .then(() => copyProjectAsset('images/spectrum.png', './src/assets/component-img-relative.png')) + .then(() => ng('build', '--aot', '--configuration=development')) + // Check paths are correctly generated. + .then(() => + expectFileToMatch('dist/test-project/browser/styles.css', 'assets/global-img-absolute.svg'), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /url\((['"]?)\/assets\/global-img-absolute\.svg\1\)/, + ), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/styles.css', /global-img-relative\.png/), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/main.js', '/assets/component-img-absolute.svg'), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/main.js', /component-img-relative\.png/), + ) + // Check files are correctly created. + .then(() => expectToFail(() => expectFileToExist(`${mediaPath}/global-img-absolute.svg`))) + .then(() => expectToFail(() => expectFileToExist(`${mediaPath}/component-img-absolute.svg`))) + .then(() => expectFileMatchToExist(mediaPath, /global-img-relative\.png/)) + .then(() => expectFileMatchToExist(mediaPath, /component-img-relative\.png/)); + + // Early exit before deploy url tests + if (!usingWebpack) { + return; + } + + // Check urls with deploy-url scheme are used as is. + return ( + Promise.resolve() + .then(() => + ng( + 'build', + '--base-href=/base/', + '--deploy-url=http://deploy.url/', + '--configuration=development', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /url\(\'\/assets\/global-img-absolute\.svg\'\)/, + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /url\(\'\/assets\/component-img-absolute\.svg\'\)/, + ), + ) + // Check urls with base-href scheme are used as is (with deploy-url). + .then(() => + ng( + 'build', + '--base-href=http://base.url/', + '--deploy-url=deploy/', + '--configuration=development', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /url\(\'\/assets\/global-img-absolute\.svg\'\)/, + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /url\(\'\/assets\/component-img-absolute\.svg\'\)/, + ), + ) + // Check urls with deploy-url and base-href scheme only use deploy-url. + .then(() => + ng( + 'build', + '--base-href=http://base.url/', + '--deploy-url=http://deploy.url/', + '--configuration=development', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /url\(\'\/assets\/global-img-absolute\.svg\'\)/, + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /url\(\'\/assets\/component-img-absolute\.svg\'\)/, + ), + ) + // Check with base-href and deploy-url flags. + .then(() => + ng( + 'build', + '--base-href=/base/', + '--deploy-url=deploy/', + '--aot', + '--configuration=development', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + '/assets/global-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/styles.css', /global-img-relative\.png/), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + '/assets/component-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /deploy\/component-img-relative\.png/, + ), + ) + // Check with identical base-href and deploy-url flags. + .then(() => + ng( + 'build', + '--base-href=/base/', + '--deploy-url=/base/', + '--aot', + '--configuration=development', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + '/assets/global-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/styles.css', /global-img-relative\.png/), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + '/assets/component-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /\/base\/component-img-relative\.png/, + ), + ) + // Check with only base-href flag. + .then(() => ng('build', '--base-href=/base/', '--aot', '--configuration=development')) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + '/assets/global-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/styles.css', /global-img-relative\.png/), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + '/assets/component-img-absolute.svg', + ), + ) + .then(() => + expectFileToMatch('dist/test-project/browser/main.js', /component-img-relative\.png/), + ) + ); +} diff --git a/tests/e2e/tests/build/disk-cache-purge.ts b/tests/e2e/tests/build/disk-cache-purge.ts new file mode 100644 index 000000000000..52edd4845038 --- /dev/null +++ b/tests/e2e/tests/build/disk-cache-purge.ts @@ -0,0 +1,30 @@ +import { join } from 'node:path'; +import { createDir, expectFileNotToExist, expectFileToExist, writeFile } from '../../utils/fs'; +import { silentNg } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + const cachePath = '.angular/cache'; + const staleCachePath = join(cachePath, 'v1.0.0'); + + // No need to include all applications code to verify disk cache existence. + await writeFile('src/main.ts', 'console.log(1);'); + + // Enable cache for all environments + await updateJsonFile('angular.json', (config) => { + config.cli ??= {}; + config.cli.cache = { + environment: 'all', + enabled: true, + path: cachePath, + }; + }); + + // Create a dummy stale disk cache directory. + await createDir(staleCachePath); + await expectFileToExist(staleCachePath); + + await silentNg('build'); + await expectFileToExist(cachePath); + await expectFileNotToExist(staleCachePath); +} diff --git a/tests/e2e/tests/build/disk-cache.ts b/tests/e2e/tests/build/disk-cache.ts new file mode 100644 index 000000000000..1873905646ca --- /dev/null +++ b/tests/e2e/tests/build/disk-cache.ts @@ -0,0 +1,56 @@ +import { expectFileNotToExist, expectFileToExist, rimraf, writeFile } from '../../utils/fs'; +import { silentNg } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +const defaultCachePath = '.angular/cache'; +const overriddenCachePath = '.cache/angular-cli'; + +export default async function () { + const originalCIValue = process.env['CI']; + + // No need to include all applications code to verify disk cache existence. + await writeFile('src/main.ts', 'console.log(1);'); + + try { + // Should be enabled by default. + process.env['CI'] = '0'; + await configureAndRunTest(); + + // Should not write cache when it's disabled + await configureAndRunTest({ enabled: false }); + await expectFileNotToExist(defaultCachePath); + + // Should not write cache by default when in CI. + process.env['CI'] = '1'; + await configureAndRunTest(); + await expectFileNotToExist(defaultCachePath); + + // Should write cache when it's enabled and 'environment' is set to 'all' or 'ci'. + await configureAndRunTest({ environment: 'all' }); + await expectFileToExist(defaultCachePath); + + // Should write cache to custom path when configured. + await configureAndRunTest({ environment: 'ci', path: overriddenCachePath }); + await expectFileNotToExist(defaultCachePath); + await expectFileToExist(overriddenCachePath); + } finally { + process.env['CI'] = originalCIValue; + } +} + +async function configureAndRunTest(cacheOptions?: { + environment?: 'ci' | 'local' | 'all'; + enabled?: boolean; + path?: string; +}): Promise { + await Promise.all([ + rimraf(overriddenCachePath), + rimraf(defaultCachePath), + updateJsonFile('angular.json', (config) => { + config.cli ??= {}; + config.cli.cache = cacheOptions; + }), + ]); + + await silentNg('build'); +} diff --git a/tests/e2e/tests/build/esbuild-unsupported.ts b/tests/e2e/tests/build/esbuild-unsupported.ts new file mode 100644 index 000000000000..a7e491aebb02 --- /dev/null +++ b/tests/e2e/tests/build/esbuild-unsupported.ts @@ -0,0 +1,16 @@ +import { join } from 'node:path'; +import { execWithEnv } from '../../utils/process'; + +export default async function () { + // TODO(bazel): fails with bazel on windows + if (process.platform.startsWith('win')) { + return; + } + + // Set the esbuild native binary path to a non-existent file to simulate a spawn error. + // The build should still succeed by falling back to the WASM variant of esbuild. + await execWithEnv('ng', ['build'], { + ...process.env, + 'ESBUILD_BINARY_PATH': join(__dirname, 'esbuild-bin-no-exist-xyz'), + }); +} diff --git a/tests/e2e/tests/build/extract-licenses.ts b/tests/e2e/tests/build/extract-licenses.ts new file mode 100644 index 000000000000..04ab08f5d49e --- /dev/null +++ b/tests/e2e/tests/build/extract-licenses.ts @@ -0,0 +1,29 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist, expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const usingWebpack = !getGlobalVariable('argv')['esbuild']; + + // Licenses should be left intact if extraction is disabled + await ng('build', '--extract-licenses=false', '--output-hashing=none'); + + if (usingWebpack) { + await expectToFail(() => expectFileToExist('dist/test-project/browser/3rdpartylicenses.txt')); + } else { + // Application builder puts the licenses at the output path root + await expectToFail(() => expectFileToExist('dist/test-project/3rdpartylicenses.txt')); + } + await expectFileToMatch('dist/test-project/browser/main.js', '@license'); + + // Licenses should be removed if extraction is enabled + await ng('build', '--extract-licenses', '--output-hashing=none'); + + if (usingWebpack) { + await expectFileToExist('dist/test-project/browser/3rdpartylicenses.txt'); + } else { + await expectFileToExist('dist/test-project/3rdpartylicenses.txt'); + } + await expectToFail(() => expectFileToMatch('dist/test-project/browser/main.js', '@license')); +} diff --git a/tests/e2e/tests/build/incremental-watch.ts b/tests/e2e/tests/build/incremental-watch.ts new file mode 100644 index 000000000000..b2d1662469bb --- /dev/null +++ b/tests/e2e/tests/build/incremental-watch.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { readdir } from 'node:fs/promises'; +import { setTimeout } from 'node:timers/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, readFile, writeFile } from '../../utils/fs'; +import { execAndWaitForOutputToMatch, waitForAnyProcessOutputToMatch } from '../../utils/process'; + +const buildReadyRegEx = /Application bundle generation complete\./; + +export default async function () { + const usingApplicationBuilder = getGlobalVariable('argv')['esbuild']; + assert( + usingApplicationBuilder, + 'Incremental watch E2E test should not be executed with Webpack.', + ); + + // Perform an initial build in watch mode + await execAndWaitForOutputToMatch( + 'ng', + ['build', '--watch', '--configuration=development'], + buildReadyRegEx, + ); + await setTimeout(500); + const initialOutputFiles = await readdir('dist/test-project/browser'); + + const originalMain = await readFile('src/main.ts'); + + // Add a dynamic import to create an additional output chunk + await Promise.all([ + waitForAnyProcessOutputToMatch(buildReadyRegEx), + await writeFile( + 'src/a.ts', + ` + export function sayHi() { + console.log('hi'); + } + `, + ), + appendToFile('src/main.ts', `\nimport('./a').then((m) => m.sayHi());`), + ]); + await setTimeout(500); + const intermediateOutputFiles = await readdir('dist/test-project/browser'); + assert( + initialOutputFiles.length < intermediateOutputFiles.length, + 'Additional chunks should be present', + ); + + // Remove usage of dynamic import which should remove the additional output chunk + await Promise.all([ + waitForAnyProcessOutputToMatch(buildReadyRegEx), + writeFile('src/main.ts', originalMain), + ]); + await setTimeout(500); + const finalOutputFiles = await readdir('dist/test-project/browser'); + assert.equal( + initialOutputFiles.length, + finalOutputFiles.length, + 'Final chunk count should be equal to initial chunk count.', + ); +} diff --git a/tests/e2e/tests/build/jit-ngmodule.ts b/tests/e2e/tests/build/jit-ngmodule.ts new file mode 100644 index 000000000000..aa6b3fda86bb --- /dev/null +++ b/tests/e2e/tests/build/jit-ngmodule.ts @@ -0,0 +1,48 @@ +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; +import { updateJsonFile, useCIDefaults } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + await ng('generate', 'app', 'test-project-two', '--no-standalone', '--skip-install'); + await useCIDefaults('test-project-two'); + + // Make prod use JIT. + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + // Setup webpack builder if esbuild is not requested on the commandline + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project-two']['architect']['build']; + if (useWebpackBuilder) { + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + buildOptimizer: false, + outputPath: 'dist/test-project-two', + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + }; + } + + // Remove bundle budgets due to the increased size from JIT + build.configurations.production = { + ...build.configurations.production, + budgets: undefined, + }; + + build.options.aot = false; + + const serve = json['projects']['test-project-two']['architect']['serve']; + serve.builder = '@angular-devkit/build-angular:dev-server'; + }); + // Test it works + await executeBrowserTest({ project: 'test-project-two', configuration: 'production' }); + await executeBrowserTest({ project: 'test-project-two', configuration: 'development' }); +} diff --git a/tests/e2e/tests/build/jit-prod.ts b/tests/e2e/tests/build/jit-prod.ts new file mode 100644 index 000000000000..b2dc9d0bdddc --- /dev/null +++ b/tests/e2e/tests/build/jit-prod.ts @@ -0,0 +1,22 @@ +import { getGlobalVariable } from '../../utils/env'; +import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + // Make prod use JIT. + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + appArchitect.build.configurations['production'].aot = false; + + // JIT applications have significantly larger sizes + appArchitect.build.configurations['production'].budgets = []; + + if (!getGlobalVariable('argv')['esbuild']) { + // The build optimizer option does not exist with the application build system + appArchitect.build.configurations['production'].buildOptimizer = false; + } + }); + + // Test it works + await executeBrowserTest({ configuration: 'production' }); +} diff --git a/tests/e2e/tests/build/lazy-load-syntax.ts b/tests/e2e/tests/build/lazy-load-syntax.ts new file mode 100644 index 000000000000..bc0a375673dc --- /dev/null +++ b/tests/e2e/tests/build/lazy-load-syntax.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 { replaceInFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + // Add lazy route. + await ng('generate', 'component', 'lazy-comp'); + await replaceInFile( + 'src/app/app.routes.ts', + 'routes: Routes = [];', + `routes: Routes = [{ + path: 'lazy', + loadComponent: () => import('./lazy-comp/lazy-comp').then(c => c.LazyComp), + }];`, + ); + + // Convert the default config to use JIT and prod to just do AOT. + // This way we can use `ng e2e` to test JIT and `ng e2e --configuration=production` to test AOT. + await updateJsonFile('angular.json', (json) => { + const buildTarget = json['projects']['test-project']['architect']['build']; + buildTarget['options']['aot'] = true; + buildTarget['configurations']['development']['aot'] = false; + }); + + const checkFn = async (page: any) => { + await page.goto(page.url() + 'lazy'); + await page.waitForFunction( + () => + !!(globalThis as any).document + .querySelector('app-lazy-comp p') + ?.textContent?.includes('lazy-comp works!'), + { timeout: 10000 }, + ); + }; + + await executeBrowserTest({ checkFn }); + await executeBrowserTest({ configuration: 'production', checkFn }); +} diff --git a/tests/e2e/tests/build/library-with-demo-app.ts b/tests/e2e/tests/build/library-with-demo-app.ts new file mode 100644 index 000000000000..8e8d14ceb54f --- /dev/null +++ b/tests/e2e/tests/build/library-with-demo-app.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { appendToFile, createDir, writeFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + await ng('generate', 'library', 'mylib'); + await createLibraryEntryPoint('secondary'); + await createLibraryEntryPoint('another'); + + // Scenario #1 where we use wildcard path mappings for secondary entry-points. + await updateJsonFile('tsconfig.json', (json) => { + json.compilerOptions.paths = { 'mylib': ['./dist/mylib'], 'mylib/*': ['./dist/mylib/*'] }; + }); + + await appendToFile( + 'src/app/app.config.ts', + ` + import * as secondary from 'mylib/secondary'; + import * as another from 'mylib/another'; + + console.log({ + secondary, + another + }); + `, + ); + + await ng('build', 'mylib'); + await ng('build'); + + // Scenario #2 where we don't use wildcard path mappings. + await updateJsonFile('tsconfig.json', (json) => { + json.compilerOptions.paths = { + 'mylib': ['./dist/mylib'], + 'mylib/secondary': ['./dist/mylib/secondary'], + 'mylib/another': ['./dist/mylib/another'], + }; + }); + + await ng('build'); +} + +async function createLibraryEntryPoint(name: string): Promise { + await createDir(`projects/mylib/${name}`); + await writeFile(`projects/mylib/${name}/index.ts`, `export const foo = 'foo';`); + + await writeFile( + `projects/mylib/${name}/ng-package.json`, + JSON.stringify({ + lib: { + entryFile: 'index.ts', + }, + }), + ); +} diff --git a/tests/e2e/tests/build/library/lib-consumption-full-aot.ts b/tests/e2e/tests/build/library/lib-consumption-full-aot.ts new file mode 100644 index 000000000000..08d114f1de4a --- /dev/null +++ b/tests/e2e/tests/build/library/lib-consumption-full-aot.ts @@ -0,0 +1,14 @@ +import { ng } from '../../../utils/process'; +import { executeBrowserTest } from '../../../utils/puppeteer'; +import { browserCheck, libraryConsumptionSetup } from './setup'; + +export default async function () { + await libraryConsumptionSetup(); + + // Build library in full mode (development) + await ng('build', 'my-lib', '--configuration=development'); + + // Check that the e2e succeeds prod and non prod mode + await executeBrowserTest({ configuration: 'production', checkFn: browserCheck }); + await executeBrowserTest({ configuration: 'development', checkFn: browserCheck }); +} diff --git a/tests/e2e/tests/build/library/lib-consumption-full-jit.ts b/tests/e2e/tests/build/library/lib-consumption-full-jit.ts new file mode 100644 index 000000000000..906a920dba44 --- /dev/null +++ b/tests/e2e/tests/build/library/lib-consumption-full-jit.ts @@ -0,0 +1,34 @@ +import { updateJsonFile } from '../../../utils/project'; +import { expectFileToMatch } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { executeBrowserTest } from '../../../utils/puppeteer'; +import { browserCheck, libraryConsumptionSetup } from './setup'; +import { getGlobalVariable } from '../../../utils/env'; + +export default async function () { + await libraryConsumptionSetup(); + + // Build library in full mode (development) + await ng('build', 'my-lib', '--configuration=development'); + + // JIT linking + await updateJsonFile('angular.json', (config) => { + const build = config.projects['test-project'].architect.build; + build.options.aot = false; + build.configurations.production.budgets = undefined; + if (!getGlobalVariable('argv')['esbuild']) { + build.configurations.production.buildOptimizer = false; + } + }); + + // Ensure app works in prod and non prod mode + await executeBrowserTest({ configuration: 'production', checkFn: browserCheck }); + await executeBrowserTest({ configuration: 'development', checkFn: browserCheck }); + + // Validate that sourcemaps for the library exists. + await ng('build', '--configuration=development'); + await expectFileToMatch( + 'dist/test-project/browser/main.js.map', + 'projects/my-lib/src/lib/my-lib.ts', + ); +} diff --git a/tests/e2e/tests/build/library/lib-consumption-partial-aot.ts b/tests/e2e/tests/build/library/lib-consumption-partial-aot.ts new file mode 100644 index 000000000000..f906be54b0e6 --- /dev/null +++ b/tests/e2e/tests/build/library/lib-consumption-partial-aot.ts @@ -0,0 +1,14 @@ +import { ng } from '../../../utils/process'; +import { executeBrowserTest } from '../../../utils/puppeteer'; +import { browserCheck, libraryConsumptionSetup } from './setup'; + +export default async function () { + await libraryConsumptionSetup(); + + // Build library in partial mode (production) + await ng('build', 'my-lib', '--configuration=production'); + + // Check that the e2e succeeds prod and non prod mode + await executeBrowserTest({ configuration: 'production', checkFn: browserCheck }); + await executeBrowserTest({ configuration: 'development', checkFn: browserCheck }); +} diff --git a/tests/e2e/tests/build/library/lib-consumption-partial-jit.ts b/tests/e2e/tests/build/library/lib-consumption-partial-jit.ts new file mode 100644 index 000000000000..503c09e525e9 --- /dev/null +++ b/tests/e2e/tests/build/library/lib-consumption-partial-jit.ts @@ -0,0 +1,27 @@ +import { updateJsonFile } from '../../../utils/project'; +import { ng } from '../../../utils/process'; +import { executeBrowserTest } from '../../../utils/puppeteer'; +import { browserCheck, libraryConsumptionSetup } from './setup'; +import { getGlobalVariable } from '../../../utils/env'; + +export default async function () { + await libraryConsumptionSetup(); + + // Build library in partial mode (production) + await ng('build', 'my-lib', '--configuration=production'); + + // JIT linking + await updateJsonFile('angular.json', (config) => { + const build = config.projects['test-project'].architect.build; + build.options.aot = false; + build.configurations.production.budgets = undefined; + + if (!getGlobalVariable('argv')['esbuild']) { + build.configurations.production.buildOptimizer = false; + } + }); + + // Check that the e2e succeeds prod and non prod mode + await executeBrowserTest({ configuration: 'production', checkFn: browserCheck }); + await executeBrowserTest({ configuration: 'development', checkFn: browserCheck }); +} diff --git a/tests/e2e/tests/build/library/lib-consumption-sourcemaps.ts b/tests/e2e/tests/build/library/lib-consumption-sourcemaps.ts new file mode 100644 index 000000000000..c0b7a5e78a73 --- /dev/null +++ b/tests/e2e/tests/build/library/lib-consumption-sourcemaps.ts @@ -0,0 +1,17 @@ +import { expectFileToMatch } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { libraryConsumptionSetup } from './setup'; + +export default async function () { + await libraryConsumptionSetup(); + + // Build library in full mode (development) + await ng('build', 'my-lib', '--configuration=development'); + + // Validate that sourcemaps for the library exists. + await ng('build', '--configuration=development'); + await expectFileToMatch( + 'dist/test-project/browser/main.js.map', + 'projects/my-lib/src/lib/my-lib.ts', + ); +} diff --git a/tests/e2e/tests/build/library/lib-unused-decorated-class-treeshake.ts b/tests/e2e/tests/build/library/lib-unused-decorated-class-treeshake.ts new file mode 100644 index 000000000000..33b221a32efe --- /dev/null +++ b/tests/e2e/tests/build/library/lib-unused-decorated-class-treeshake.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert'; +import { appendToFile, expectFileToExist, expectFileToMatch, readFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { libraryConsumptionSetup } from './setup'; +import { updateJsonFile } from '../../../utils/project'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await ng('cache', 'off'); + await libraryConsumptionSetup(); + + // Add an unused class as part of the public api. + await appendToFile( + 'projects/my-lib/src/lib/my-lib.ts', + ` + function something() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + console.log("someDecorator"); + }; + } + + export class ExampleClass { + @something() + method() {} + } + `, + ); + + // build the lib + await ng('build', 'my-lib', '--configuration=production'); + const packageJson = JSON.parse(await readFile('dist/my-lib/package.json')); + assert.equal(packageJson.sideEffects, false); + + // build the app + await ng('build', 'test-project', '--configuration=production', '--output-hashing=none'); + // Output should not contain `ExampleClass` as the library is marked as side-effect free. + await expectFileToExist('dist/test-project/browser/main.js'); + await expectToFail(() => expectFileToMatch('dist/test-project/browser/main.js', 'someDecorator')); + + // Mark library as side-effectful. + await updateJsonFile('dist/my-lib/package.json', (packageJson) => { + packageJson.sideEffects = true; + }); + + // build the app + await ng('build', 'test-project', '--configuration=production', '--output-hashing=none'); + // Output should contain `ExampleClass` as the library is marked as side-effectful. + await expectFileToMatch('dist/test-project/browser/main.js', 'someDecorator'); +} diff --git a/tests/e2e/tests/build/library/setup.ts b/tests/e2e/tests/build/library/setup.ts new file mode 100644 index 000000000000..621b740cf4bc --- /dev/null +++ b/tests/e2e/tests/build/library/setup.ts @@ -0,0 +1,45 @@ +import type { Page } from 'puppeteer'; +import { writeMultipleFiles } from '../../../utils/fs'; +import { silentNg } from '../../../utils/process'; + +export async function libraryConsumptionSetup(): Promise { + await silentNg('generate', 'library', 'my-lib'); + + // Force an external template + await writeMultipleFiles({ + 'projects/my-lib/src/lib/my-lib.html': `

my-lib works!

`, + 'projects/my-lib/src/lib/my-lib.ts': `import { Component } from '@angular/core'; + + @Component({ + selector: 'lib-my-lib', + templateUrl: './my-lib.html', + }) + export class MyLibComponent {}`, + './src/app/app.ts': ` + import { Component } from '@angular/core'; + import { MyLibComponent } from 'my-lib'; + + @Component({ + selector: 'app-root', + template: '', + imports: [MyLibComponent], + }) + export class App { + title = 'test-project'; + + constructor() { + } + } + `, + }); +} + +export async function browserCheck(page: Page): Promise { + await page.waitForFunction( + () => + !!(globalThis as any).document + .querySelector('lib-my-lib p') + ?.textContent?.includes('my-lib works!'), + { timeout: 10000 }, + ); +} diff --git a/tests/e2e/tests/build/material.ts b/tests/e2e/tests/build/material.ts new file mode 100644 index 000000000000..bc4862f735fc --- /dev/null +++ b/tests/e2e/tests/build/material.ts @@ -0,0 +1,89 @@ +import { appendFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { readFile, replaceInFile } from '../../utils/fs'; +import { + getActivePackageManager, + installPackage, + installWorkspacePackages, +} from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { isPrereleaseCli, updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +const snapshots = require('../../ng-snapshot/package.json'); + +export default async function () { + const isPrerelease = await isPrereleaseCli(); + let tag = isPrerelease ? '@next' : ''; + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Angular material adds dependencies on other Angular packages + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries(snapshots.dependencies)) { + if (name in dependencies) { + dependencies[name] = version; + } + } + + dependencies['@angular/material-moment-adapter'] = + snapshots.dependencies['@angular/material-moment-adapter']; + }); + await installWorkspacePackages(); + } else { + if (!tag) { + const installedMaterialVersion = JSON.parse(await readFile('package.json'))['dependencies'][ + '@angular/material' + ]; + tag = `@${installedMaterialVersion}`; + } + await installPackage(`@angular/material-moment-adapter${tag}`); + } + + await installPackage('moment'); + + await ng('build'); + + // Ensure moment adapter works (uses unique importing mechanism for moment) + // Issue: https://github.com/angular/angular-cli/issues/17320 + await replaceInFile( + 'src/app/app.config.ts', + `from '@angular/core';`, + ` + from '@angular/core'; + import { + MomentDateAdapter, + MAT_MOMENT_DATE_FORMATS + } from '@angular/material-moment-adapter'; + import { + DateAdapter, + MAT_DATE_LOCALE, + MAT_DATE_FORMATS + } from '@angular/material/core'; + `, + ); + + await replaceInFile( + 'src/app/app.config.ts', + `provideRouter(routes)`, + `provideRouter(routes), + { + provide: DateAdapter, + useClass: MomentDateAdapter, + deps: [MAT_DATE_LOCALE] + }, + { + provide: MAT_DATE_FORMATS, + useValue: MAT_MOMENT_DATE_FORMATS + }`, + ); + + await executeBrowserTest({ configuration: 'production' }); +} diff --git a/tests/e2e/tests/build/multiple-configs.ts b/tests/e2e/tests/build/multiple-configs.ts new file mode 100644 index 000000000000..dbbeba4658e3 --- /dev/null +++ b/tests/e2e/tests/build/multiple-configs.ts @@ -0,0 +1,63 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + // TODO: Restructure to support application builder option + // This only needs to be tested once since it is really testing the CLI itself and not the builders + if (getGlobalVariable('argv')['esbuild']) { + return; + } + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + // These are the default options, that we'll overwrite in subsequent configs. + // sourceMap defaults to true + appArchitect['build'] = { + ...appArchitect['build'], + defaultConfiguration: undefined, + options: { + ...appArchitect['build'].options, + optimization: false, + sourceMap: true, + outputHashing: 'none', + vendorChunk: true, + styles: ['src/styles.css'], + scripts: [], + budgets: [], + }, + configurations: { + development: { + sourceMap: true, + }, + one: { + assets: [], + }, + two: { + sourceMap: false, + }, + }, + }; + + return workspaceJson; + }); + + // Test the base configuration. + await ng('build', '--configuration=development'); + await expectFileToExist('dist/test-project/browser/favicon.ico'); + await expectFileToExist('dist/test-project/browser/main.js.map'); + await expectFileToExist('dist/test-project/browser/vendor.js'); + await ng('build'); + await expectFileToExist('dist/test-project/browser/styles.css'); + // Use two configurations. + await ng('build', '--configuration=one,two', '--vendor-chunk=false'); + await expectToFail(() => expectFileToExist('dist/test-project/browser/favicon.ico')); + await expectToFail(() => expectFileToExist('dist/test-project/browser/main.js.map')); + // Use two configurations and two overrides, one of which overrides a config. + await ng('build', '--configuration=one,two', '--vendor-chunk=false', '--source-map=true'); + await expectToFail(() => expectFileToExist('dist/test-project/browser/favicon.ico')); + await expectFileToExist('dist/test-project/browser/main.js.map'); + await expectToFail(() => expectFileToExist('dist/test-project/browser/vendor.js')); +} diff --git a/tests/e2e/tests/build/output-dir.ts b/tests/e2e/tests/build/output-dir.ts new file mode 100644 index 000000000000..0a93ebae67b4 --- /dev/null +++ b/tests/e2e/tests/build/output-dir.ts @@ -0,0 +1,29 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist } from '../../utils/fs'; +import { expectGitToBeClean } from '../../utils/git'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + const usingWebpack = !getGlobalVariable('argv')['esbuild']; + + return ng('build', '--output-path', 'build-output', '--configuration=development') + .then(() => expectFileToExist(`./build-output/${usingWebpack ? '' : 'browser/'}index.html`)) + .then(() => expectFileToExist(`./build-output/${usingWebpack ? '' : 'browser/'}main.js`)) + .then(() => expectToFail(expectGitToBeClean)) + .then(() => + updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.outputPath = 'config-build-output'; + }), + ) + .then(() => ng('build', '--configuration=development')) + .then(() => + expectFileToExist(`./config-build-output/${usingWebpack ? '' : 'browser/'}index.html`), + ) + .then(() => expectFileToExist(`./config-build-output/${usingWebpack ? '' : 'browser/'}main.js`)) + .then(() => expectToFail(expectGitToBeClean)); +} diff --git a/tests/e2e/tests/build/poll.ts b/tests/e2e/tests/build/poll.ts new file mode 100644 index 000000000000..3b72b7c2d4e6 --- /dev/null +++ b/tests/e2e/tests/build/poll.ts @@ -0,0 +1,29 @@ +import { setTimeout } from 'node:timers/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile } from '../../utils/fs'; +import { waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { ngServe } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +const webpackGoodRegEx = getGlobalVariable('argv')['esbuild'] + ? /Application bundle generation complete\./ + : / Compiled successfully\./; + +export default async function () { + await ngServe('--poll=10000'); + + // Wait before editing a file. + // Editing too soon seems to trigger a rebuild and throw polling out of whack. + await setTimeout(3000); + await appendToFile('src/main.ts', 'console.log(1);'); + + // We have to wait poll time + rebuild build time for the regex match. + await waitForAnyProcessOutputToMatch(webpackGoodRegEx, 14000); + + // No rebuilds should occur for a while + await appendToFile('src/main.ts', 'console.log(1);'); + await expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000)); + + // But a rebuild should happen roughly within the 10 second window. + await waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000); +} diff --git a/tests/e2e/tests/build/prerender/discover-routes-ngmodule.ts b/tests/e2e/tests/build/prerender/discover-routes-ngmodule.ts new file mode 100644 index 000000000000..9d4843e00f7d --- /dev/null +++ b/tests/e2e/tests/build/prerender/discover-routes-ngmodule.ts @@ -0,0 +1,150 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile, useSha } from '../../../utils/project'; + +export default async function () { + const projectName = 'test-project-two'; + await ng('generate', 'application', projectName, '--no-standalone', '--skip-install'); + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // Setup webpack builder if esbuild is not requested on the commandline + await updateJsonFile('angular.json', (json) => { + const build = json['projects'][projectName]['architect']['build']; + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + buildOptimizer: false, + }; + }); + } + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng( + 'add', + '@angular/ssr', + '--project', + projectName, + '--skip-confirmation', + '--skip-install', + ); + + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + `projects/${projectName}/src/app/app-routing-module.ts`, + ` + import { NgModule } from '@angular/core'; + import { RouterModule, Routes } from '@angular/router'; + import { One} from './one/one'; + import { TwoChildOne } from './two-child-one/two-child-one'; + import { TwoChildTwo } from './two-child-two/two-child-two'; + + const routes: Routes = [ + { + path: '', + component: One, + }, + { + path: 'two', + children: [ + { + path: 'two-child-one', + component: TwoChildOne, + }, + { + path: 'two-child-two', + component: TwoChildTwo, + }, + ], + }, + ]; + + @NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + }) + export class AppRoutingModule {} + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['one', 'two-child-one', 'two-child-two']; + + for (const componentName of componentNames) { + await ng('generate', 'component', componentName, '--project', projectName); + } + + // Generate lazy routes + const lazyModules: [route: string, moduleName: string][] = [ + ['lazy-one', 'app-module'], + ['lazy-one-child', 'lazy-one/lazy-one-module'], + ['lazy-two', 'app-module'], + ]; + + for (const [route, moduleName] of lazyModules) { + await ng( + 'generate', + 'module', + route, + '--route', + route, + '--module', + moduleName, + '--project', + projectName, + ); + } + + // Prerender pages + if (useWebpackBuilder) { + await ng('run', `${projectName}:prerender:production`); + await runExpects(); + + return; + } + + await ng('build', projectName, '--configuration=production'); + await runExpects(); + + // Test also JIT mode. + await ng('build', projectName, '--configuration=development', '--no-aot'); + + await runExpects(); + + async function runExpects(): Promise { + const expects: Record = { + 'index.html': 'one works!', + 'two/index.html': 'router-outlet', + 'two/two-child-one/index.html': 'two-child-one works!', + 'two/two-child-two/index.html': 'two-child-two works!', + 'lazy-one/index.html': 'lazy-one works!', + 'lazy-one/lazy-one-child/index.html': 'lazy-one-child works!', + 'lazy-two/index.html': 'lazy-two works!', + }; + + if (!useWebpackBuilder) { + expects['index.csr.html'] = ''; + } + + const distPath = 'dist/' + projectName + '/browser'; + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join(distPath, filePath), fileMatch); + } + } +} diff --git a/tests/e2e/tests/build/prerender/discover-routes-standalone.ts b/tests/e2e/tests/build/prerender/discover-routes-standalone.ts new file mode 100644 index 000000000000..71a3ba2fb15d --- /dev/null +++ b/tests/e2e/tests/build/prerender/discover-routes-standalone.ts @@ -0,0 +1,126 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToMatch, readFile, writeFile } from '../../../utils/fs'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { useSha } from '../../../utils/project'; +import { deepStrictEqual } from 'node:assert'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { One } from './one/one'; + import { TwoChildOne } from './two-child-one/two-child-one'; + import { TwoChildTwo } from './two-child-two/two-child-two'; + + export const routes: Routes = [ + { + path: '', + component: One, + }, + { + path: 'two', + children: [ + { + path: 'two-child-one', + component: TwoChildOne, + }, + { + path: 'two-child-two', + component: TwoChildTwo, + }, + ], + }, + { + path: 'lazy-one', + children: [ + { + path: '', + loadComponent: () => import('./lazy-one/lazy-one').then(c => c.LazyOne), + }, + { + path: 'lazy-one-child', + loadComponent: () => import('./lazy-one-child/lazy-one-child').then(c => c.LazyOneChild), + }, + ], + }, + { + path: 'lazy-two', + loadComponent: () => import('./lazy-two/lazy-two').then(c => c.LazyTwo), + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = [ + 'one', + 'two-child-one', + 'two-child-two', + 'lazy-one', + 'lazy-one-child', + 'lazy-two', + ]; + + for (const componentName of componentNames) { + await ng('generate', 'component', componentName); + } + + // Prerender pages + if (useWebpackBuilder) { + await ng('run', 'test-project:prerender:production'); + await runExpects(); + + return; + } + + await ng('build', '--configuration=production', '--prerender'); + await runExpects(); + + // Test also JIT mode. + await ng('build', '--configuration=development', '--prerender', '--no-aot'); + await runExpects(); + + async function runExpects(): Promise { + const expects: Record = { + 'index.html': 'one works!', + 'two/index.html': 'router-outlet', + 'two/two-child-one/index.html': 'two-child-one works!', + 'two/two-child-two/index.html': 'two-child-two works!', + 'lazy-one/index.html': 'lazy-one works!', + 'lazy-one/lazy-one-child/index.html': 'lazy-one-child works!', + 'lazy-two/index.html': 'lazy-two works!', + }; + + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch); + } + + if (!useWebpackBuilder) { + // prerendered-routes.json file is only generated when using esbuild. + const generatedRoutesStats = await readFile('dist/test-project/prerendered-routes.json'); + deepStrictEqual(JSON.parse(generatedRoutesStats), { + routes: { + '/': {}, + '/lazy-one': {}, + '/lazy-one/lazy-one-child': {}, + '/lazy-two': {}, + '/two': {}, + '/two/two-child-one': {}, + '/two/two-child-two': {}, + }, + }); + } + } +} diff --git a/tests/e2e/tests/build/prerender/error-with-sourcemaps.ts b/tests/e2e/tests/build/prerender/error-with-sourcemaps.ts new file mode 100644 index 000000000000..875638877fbd --- /dev/null +++ b/tests/e2e/tests/build/prerender/error-with-sourcemaps.ts @@ -0,0 +1,52 @@ +import { ng } from '../../../utils/process'; +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { match } from 'node:assert'; +import { expectToFail } from '../../../utils/utils'; +import { useSha } from '../../../utils/project'; +import { installWorkspacePackages } from '../../../utils/packages'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + return; + } + + // Forcibly remove in case another test doesn't clean itself up. + await rimraf('node_modules/@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + 'src/app/app.ts': ` + import { Component, signal } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + + @Component({ + selector: 'app-root', + imports: [CommonModule, RouterOutlet], + templateUrl: './app.html', + styleUrls: ['./app.css'] + }) + export class App { + protected readonly title = signal('test-ssr'); + + constructor() { + console.log(window) + } + } + `, + }); + + const { message } = await expectToFail(() => + ng('build', '--configuration', 'development', '--prerender'), + ); + match( + message, + // When babel is used it will add names to the sourcemap and `constructor` will be used in the stack trace. + // This will currently only happen if AOT and script optimizations are set which enables advanced optimizations. + /window is not defined[.\s\S]*(?:constructor|_App) \(.*app\.ts\:\d+:\d+\)/, + ); +} diff --git a/tests/e2e/tests/build/prerender/http-requests-assets.ts b/tests/e2e/tests/build/prerender/http-requests-assets.ts new file mode 100644 index 000000000000..71288b3c242e --- /dev/null +++ b/tests/e2e/tests/build/prerender/http-requests-assets.ts @@ -0,0 +1,92 @@ +import { ng } from '../../../utils/process'; +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToMatch, writeMultipleFiles } from '../../../utils/fs'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // Not supported by the webpack based builder. + return; + } + + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Add http client and route + 'src/app/app.config.ts': ` + import { ApplicationConfig } from '@angular/core'; + import { provideRouter } from '@angular/router'; + + import {Home} from './home/home'; + import { provideClientHydration } from '@angular/platform-browser'; + import { provideHttpClient, withFetch } from '@angular/common/http'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideRouter([{ + path: '', + component: Home, + }]), + provideClientHydration(), + provideHttpClient(withFetch()), + ], + }; + `, + + // Add asset + 'public/media.json': JSON.stringify({ dataFromAssets: true }), + 'public/media with-space.json': JSON.stringify({ dataFromAssetsWithSpace: true }), + + // Update component to do an HTTP call to asset. + 'src/app/app.ts': ` + import { ChangeDetectorRef, Component, inject } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + import { HttpClient } from '@angular/common/http'; + + @Component({ + selector: 'app-root', + imports: [CommonModule, RouterOutlet], + template: \` +

{{ data | json }}

+

{{ dataWithSpace | json }}

+ + \`, + }) + export class App { + data: any; + dataWithSpace: any; + private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + const http = inject(HttpClient); + http.get('/media.json').subscribe((d) => { + this.data = d; + this.cdr.markForCheck(); + }); + + http.get('/media%20with-space.json').subscribe((d) => { + this.dataWithSpace = d; + this.cdr.markForCheck(); + }); + } + } + `, + }); + + await ng('generate', 'component', 'home'); + await ng('build', '--configuration=production', '--prerender'); + await expectFileToMatch( + 'dist/test-project/browser/index.html', + /

{[\S\s]*"dataFromAssets":[\s\S]*true[\S\s]*}<\/p>/, + ); + await expectFileToMatch( + 'dist/test-project/browser/index.html', + /

{[\S\s]*"dataFromAssetsWithSpace":[\s\S]*true[\S\s]*}<\/p>/, + ); +} diff --git a/tests/e2e/tests/build/prod-build.ts b/tests/e2e/tests/build/prod-build.ts new file mode 100644 index 000000000000..dee45876e379 --- /dev/null +++ b/tests/e2e/tests/build/prod-build.ts @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import { statSync } from 'node:fs'; +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist, expectFileToMatch, readFile } from '../../utils/fs'; +import { noSilentNg } from '../../utils/process'; + +function verifySize(bundle: string, baselineBytes: number) { + const size = statSync(`dist/test-project/browser/${bundle}`).size; + const percentageBaseline = (baselineBytes * 10) / 100; + const maxSize = baselineBytes + percentageBaseline; + const minSize = baselineBytes - percentageBaseline; + + assert( + size < maxSize, + `Expected ${bundle} size to be less than ${maxSize / 1024}Kb but it was ${size / 1024}Kb.`, + ); + + assert( + size > minSize, + `Expected ${bundle} size to be greater than ${minSize / 1024}Kb but it was ${size / 1024}Kb.`, + ); +} + +export default async function () { + await noSilentNg('build'); + await expectFileToExist(join(process.cwd(), 'dist')); + // Check for cache busting hash script src + if (getGlobalVariable('argv')['esbuild']) { + // esbuild uses an 8 character hash and a dash as separator + await expectFileToMatch('dist/test-project/browser/index.html', /main-[0-9a-zA-Z]{8}\.js/); + await expectFileToMatch('dist/test-project/browser/index.html', /styles-[0-9a-zA-Z]{8}\.css/); + await expectFileToMatch('dist/test-project/3rdpartylicenses.txt', /MIT/); + } else { + await expectFileToMatch('dist/test-project/browser/index.html', /main\.[0-9a-zA-Z]{16}\.js/); + await expectFileToMatch('dist/test-project/browser/index.html', /styles\.[0-9a-zA-Z]{16}\.css/); + await expectFileToMatch('dist/test-project/browser/3rdpartylicenses.txt', /MIT/); + } + + const indexContent = await readFile('dist/test-project/browser/index.html'); + const mainSrcRegExp = getGlobalVariable('argv')['esbuild'] + ? /src="(main-[0-9a-zA-Z]{8}\.js)"/ + : /src="(main\.[0-9a-zA-Z]{16}\.js)"/; + const mainPath = indexContent.match(mainSrcRegExp)![1]; + + // Size checks in bytes + verifySize(mainPath, 210000); +} diff --git a/tests/e2e/tests/build/progress-and-stats.ts b/tests/e2e/tests/build/progress-and-stats.ts new file mode 100644 index 000000000000..940179df052e --- /dev/null +++ b/tests/e2e/tests/build/progress-and-stats.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; + +export default async function () { + const { stderr: stderrProgress, stdout } = await ng('build', '--progress'); + assert.match(stdout, /Initial total/); + assert.match(stdout, /Estimated transfer size/); + + let logs; + if (getGlobalVariable('argv')['esbuild']) { + assert.match(stdout, /Building\.\.\./); + + return; + } else { + logs = [ + 'Browser application bundle generation complete', + 'Copying assets complete', + 'Index html generation complete', + ]; + } + + for (const log of logs) { + assert.match(stderrProgress, new RegExp(log)); + } + + const { stderr: stderrNoProgress } = await ng('build', '--no-progress'); + for (const log of logs) { + assert.doesNotMatch(stderrNoProgress, new RegExp(log)); + } +} diff --git a/tests/e2e/tests/build/project-name.ts b/tests/e2e/tests/build/project-name.ts new file mode 100644 index 000000000000..309ba4d8e897 --- /dev/null +++ b/tests/e2e/tests/build/project-name.ts @@ -0,0 +1,8 @@ +import { silentNg } from '../../utils/process'; + +export default async function () { + // Named Development build + await silentNg('build', 'test-project', '--configuration=development'); + await silentNg('build', '--configuration=development', 'test-project', '--no-progress'); + await silentNg('build', '--configuration=development', '--no-progress', 'test-project'); +} diff --git a/tests/e2e/tests/build/rebuild-deps-type-check.ts b/tests/e2e/tests/build/rebuild-deps-type-check.ts new file mode 100644 index 000000000000..1f4964f6689b --- /dev/null +++ b/tests/e2e/tests/build/rebuild-deps-type-check.ts @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, prependToFile, writeFile } from '../../utils/fs'; +import { execAndWaitForOutputToMatch, waitForAnyProcessOutputToMatch } from '../../utils/process'; + +const doneRe = getGlobalVariable('argv')['esbuild'] + ? /Application bundle generation complete\./ + : / Compiled successfully\.|: Failed to compile\./; +const errorRe = /Error/i; + +export default function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + if (process.platform.startsWith('win')) { + return Promise.resolve(); + } + + return ( + Promise.resolve() + // Create and import files. + .then(() => + writeFile( + 'src/funky2.ts', + ` + export function funky2(value: string): string { + return value + 'hello'; + } + `, + ), + ) + .then(() => + writeFile( + 'src/funky.ts', + ` + export * from './funky2'; + `, + ), + ) + .then(() => + prependToFile( + 'src/main.ts', + ` + import { funky2 } from './funky'; + `, + ), + ) + .then(() => + appendToFile( + 'src/main.ts', + ` + console.log(funky2('town')); + `, + ), + ) + // Should trigger a rebuild, no error expected. + .then(() => execAndWaitForOutputToMatch('ng', ['serve'], doneRe)) + // Make an invalid version of the file. + // Should trigger a rebuild, this time an error is expected. + .then(() => + Promise.all([ + waitForAnyProcessOutputToMatch(errorRe, 20000), + writeFile( + 'src/funky2.ts', + ` + export function funky2(value: number): number { + return value + 1; + } + `, + ), + ]), + ) + .then((results) => { + const { stderr } = results[0]; + assert.match( + stderr, + /Argument of type 'string' is not assignable to parameter of type 'number'/, + ); + }) + // Change an UNRELATED file and the error should still happen. + // Should trigger a rebuild, this time an error is also expected. + .then(() => + Promise.all([ + waitForAnyProcessOutputToMatch(errorRe, 20000), + appendToFile( + 'src/app/app.config.ts', + ` + function anything(): number { return 1; } + `, + ), + ]), + ) + .then((results) => { + const { stderr } = results[0]; + assert.match( + stderr, + /Argument of type 'string' is not assignable to parameter of type 'number'/, + ); + }) + // Fix the error! + .then(() => + Promise.all([ + waitForAnyProcessOutputToMatch(doneRe, 20000), + writeFile( + 'src/funky2.ts', + ` + export function funky2(value: string): string { + return value + 'hello'; + } + `, + ), + ]), + ) + .then((results) => { + const { stderr } = results[0]; + assert.doesNotMatch( + stderr, + /Argument of type 'string' is not assignable to parameter of type 'number'/, + ); + }) + ); +} diff --git a/tests/e2e/tests/build/rebuild-dot-dirname.ts b/tests/e2e/tests/build/rebuild-dot-dirname.ts new file mode 100644 index 000000000000..ed5f12ed94c1 --- /dev/null +++ b/tests/e2e/tests/build/rebuild-dot-dirname.ts @@ -0,0 +1,63 @@ +import { setTimeout } from 'node:timers/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, createDir, rimraf } from '../../utils/fs'; +import { installWorkspacePackages } from '../../utils/packages'; +import { killAllProcesses, ng, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { ngServe, updateJsonFile, useSha } from '../../utils/project'; + +const goodRegEx = getGlobalVariable('argv')['esbuild'] + ? /Application bundle generation complete\./ + : / Compiled successfully\./; + +export default async function () { + const originalCwd = process.cwd(); + // Delete angular.json so that we can create a new app. + await rimraf('angular.json'); + await createDir('./.subdirectory'); + + try { + process.chdir('./.subdirectory'); + + await ng('new', 'subdirectory-test-project', '--skip-install'); + process.chdir('./subdirectory-test-project'); + + await useSha(); + await installWorkspacePackages(); + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['subdirectory-test-project']['architect']['build']; + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + outputPath: 'dist/subdirectory-test-project', + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + buildOptimizer: false, + }; + + const serve = json['projects']['subdirectory-test-project']['architect']['serve']; + serve.builder = '@angular-devkit/build-angular:dev-server'; + }); + } + + await ngServe(); + + // Wait before editing a file. + await setTimeout(1000); + await appendToFile('src/main.ts', 'console.log(1);'); + await waitForAnyProcessOutputToMatch(goodRegEx); + } finally { + process.chdir(originalCwd); + await killAllProcesses(); + await setTimeout(100); + } +} diff --git a/tests/e2e/tests/build/rebuild-replacements.ts b/tests/e2e/tests/build/rebuild-replacements.ts new file mode 100644 index 000000000000..8f2286ae36b7 --- /dev/null +++ b/tests/e2e/tests/build/rebuild-replacements.ts @@ -0,0 +1,41 @@ +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, createDir, writeMultipleFiles } from '../../utils/fs'; +import { waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { ngServe, updateJsonFile } from '../../utils/project'; + +const webpackGoodRegEx = getGlobalVariable('argv')['esbuild'] + ? /Application bundle generation complete\./ + : / Compiled successfully./; + +export default async function () { + if (process.platform.startsWith('win')) { + return; + } + + await createDir('src/environments'); + + await writeMultipleFiles({ + 'src/environments/environment.ts': `export const env = 'dev';`, + 'src/environments/environment.prod.ts': `export const env = 'prod';`, + 'src/main.ts': ` + import { env } from './environments/environment'; + console.log(env); + `, + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.configurations.production.fileReplacements = [ + { + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }, + ]; + }); + + await ngServe('--configuration=production'); + + // Should trigger a rebuild. + await appendToFile('src/environments/environment.prod.ts', `console.log('PROD');`); + await waitForAnyProcessOutputToMatch(webpackGoodRegEx); +} diff --git a/tests/e2e/tests/build/rebuild-symlink.ts b/tests/e2e/tests/build/rebuild-symlink.ts new file mode 100644 index 000000000000..2cbe41aded25 --- /dev/null +++ b/tests/e2e/tests/build/rebuild-symlink.ts @@ -0,0 +1,41 @@ +import { symlink } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; +import { execAndWaitForOutputToMatch, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +const buildReadyRegEx = getGlobalVariable('argv')['esbuild'] + ? /Application bundle generation complete\./ + : /Build at: /; + +export default async function () { + // TODO: Disabled pending investigation. Steps work outside of test + if (getGlobalVariable('argv')['esbuild']) { + return; + } + + await updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].architect.build.options.preserveSymlinks = true; + }); + + await writeMultipleFiles({ + 'src/link-source.ts': '// empty file', + 'src/main.ts': `import './link-dest';`, + }); + + await symlink(resolve('src/link-source.ts'), resolve('src/link-dest.ts')); + + await execAndWaitForOutputToMatch( + 'ng', + ['build', '--watch', '--configuration=development'], + buildReadyRegEx, + ); + + // Trigger a rebuild + await Promise.all([ + waitForAnyProcessOutputToMatch(buildReadyRegEx), + appendToFile('src/link-source.ts', `\nconsole.log('foo-bar');`), + ]); + await expectFileToMatch('dist/test-project/browser/main.js', `console.log('foo-bar')`); +} diff --git a/tests/e2e/tests/build/relative-sourcemap.ts b/tests/e2e/tests/build/relative-sourcemap.ts new file mode 100644 index 000000000000..209e29aabd76 --- /dev/null +++ b/tests/e2e/tests/build/relative-sourcemap.ts @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import { isAbsolute } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + // General secondary application project + await ng('generate', 'application', 'secondary-project', '--skip-install'); + // Setup webpack builder if esbuild is not requested on the commandline + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['secondary-project']['architect']['build']; + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + outputPath: 'dist/secondary-project', + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + buildOptimizer: false, + }; + }); + } + + await ng('build', 'secondary-project', '--configuration=development'); + await ng('build', '--output-hashing=none', '--source-map', '--configuration=development'); + + const sourceMapPath = getGlobalVariable('argv')['esbuild'] + ? './dist/secondary-project/browser/main.js.map' + : './dist/secondary-project/main.js.map'; + const content = fs.readFileSync(sourceMapPath, 'utf8'); + const { sources } = JSON.parse(content) as { sources: string[] }; + let mainFileFound = false; + for (const source of sources) { + assert(!isAbsolute(source), `Expected ${source} to be relative.`); + + if (source.endsWith('main.ts')) { + mainFileFound = true; + assert( + source === 'projects/secondary-project/src/main.ts' || + source === './projects/secondary-project/src/main.ts', + `Expected main file ${source} to be relative to the workspace root.`, + ); + } + } + + assert(mainFileFound, 'Could not find the main file in the application sourcemap sources array.'); +} diff --git a/tests/e2e/tests/build/scripts-output-hashing.ts b/tests/e2e/tests/build/scripts-output-hashing.ts new file mode 100644 index 000000000000..8b34662c485e --- /dev/null +++ b/tests/e2e/tests/build/scripts-output-hashing.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { getGlobalVariable } from '../../utils/env'; +import { + expectFileMatchToExist, + expectFileToMatch, + writeFile, + writeMultipleFiles, +} from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +function getScriptsFilename(): Promise { + if (getGlobalVariable('argv')['esbuild']) { + return expectFileMatchToExist('dist/test-project/browser/', /external-module-[0-9A-Z]{8}\.js/); + } else { + return expectFileMatchToExist( + 'dist/test-project/browser/', + /external-module\.[0-9a-f]{16}\.js/, + ); + } +} + +export default async function () { + // verify content hash is based on code after optimizations + await writeMultipleFiles({ + 'src/script.js': 'try { console.log(); } catch {}', + }); + await updateJsonFile('angular.json', (configJson) => { + const build = configJson.projects['test-project'].architect.build; + build.options['scripts'] = [ + { + input: 'src/script.js', + inject: true, + bundleName: 'external-module', + }, + ]; + build.configurations['production'].outputHashing = 'all'; + configJson['cli'] = { cache: { enabled: 'false' } }; + }); + + // Chrome 65 does not support optional catch in try/catch blocks. + await writeFile('.browserslistrc', 'Chrome 65'); + + await ng('build', '--configuration=production'); + const filenameBuild1 = await getScriptsFilename(); + await expectFileToMatch( + `dist/test-project/browser/${filenameBuild1}`, + 'try{console.log()}catch(c){}', + ); + + await writeFile('.browserslistrc', 'last 1 Chrome version'); + + await ng('build', '--configuration=production'); + const filenameBuild2 = await getScriptsFilename(); + await expectFileToMatch( + `dist/test-project/browser/${filenameBuild2}`, + 'try{console.log()}catch{}', + ); + + assert.notEqual( + filenameBuild1, + filenameBuild2, + 'Contents of the built file changed between builds, but the content hash stayed the same!', + ); +} diff --git a/tests/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts b/tests/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts new file mode 100644 index 000000000000..19e7dcd28b60 --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts @@ -0,0 +1,162 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { updateJsonFile, updateServerFileForEsbuild, useSha } from '../../../utils/project'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + // forcibly remove in case another test doesn't clean itself up + await rimraf('node_modules/@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + if (!useWebpackBuilder) { + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.options.outputMode = undefined; + build.configurations.production.prerender = false; + }); + + await updateServerFileForEsbuild('src/server.ts'); + } + + await writeMultipleFiles({ + 'src/app/app.css': `div { color: #000 }`, + 'src/styles.css': `* { color: #000 }`, + 'src/main.ts': `import { bootstrapApplication } from '@angular/platform-browser'; + import { App } from './app/app'; + import { appConfig } from './app/app.config'; + + (window as any)['doBootstrap'] = () => { + bootstrapApplication(App, appConfig).catch((err) => console.error(err)); + }; + `, + 'src/index.html': ` + + + + + + + + + + + `, + 'e2e/src/app.e2e-spec.ts': ` + import { browser, by, element } from 'protractor'; + import * as webdriver from 'selenium-webdriver'; + + function verifyNoBrowserErrors() { + return browser + .manage() + .logs() + .get('browser') + .then(function (browserLog: any[]) { + const errors: any[] = []; + browserLog.filter((logEntry) => { + const msg = logEntry.message; + console.log('>> ' + msg); + if (logEntry.level.value >= webdriver.logging.Level.INFO.value) { + errors.push(msg); + } + }); + expect(errors).toEqual([]); + }); + } + + describe('Hello world E2E Tests', () => { + beforeAll(async () => { + await browser.waitForAngularEnabled(false); + }); + + it('should display: Welcome', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + expect( + await element(by.css('style[ng-app-id="ng"]')).getText() + ).not.toBeNull(); + + // Test the contents from the server. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Bootstrap the client side app. + await browser.executeScript('doBootstrap()'); + + // Retest the contents after the client bootstraps. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Make sure the server styles got replaced by client side ones. + expect( + await element(by.css('style[ng-app-id="ng"]')).isPresent() + ).toBeFalsy(); + expect(await element(by.css('style')).getText()).toMatch(''); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + + it('stylesheets should be configured to load asynchronously', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + const linkTag = await browser.driver.findElement( + by.css('link[rel="stylesheet"]') + ); + expect(await linkTag.getAttribute('media')).toMatch('all'); + expect(await linkTag.getAttribute('ngCspMedia')).toBeNull(); + expect(await linkTag.getAttribute('onload')).toBeNull(); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + + it('style tags all have a nonce attribute', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + for (const s of await browser.driver.findElements(by.css('style'))) { + expect(await s.getAttribute('nonce')).toBe('{% nonce %}'); + } + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + }); + `, + }); + + async function spawnServer(): Promise { + const port = await findFreePort(); + + const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; + + await execAndWaitForOutputToMatch( + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; + } + + await ng('build'); + + if (useWebpackBuilder) { + // Build server code + await ng('run', 'test-project:server'); + } + + const port = await spawnServer(); + await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); +} diff --git a/tests/e2e/tests/build/server-rendering/express-engine-ngmodule.ts b/tests/e2e/tests/build/server-rendering/express-engine-ngmodule.ts new file mode 100644 index 000000000000..f05d2182bbd2 --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/express-engine-ngmodule.ts @@ -0,0 +1,181 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { + updateJsonFile, + updateServerFileForEsbuild, + useCIChrome, + useCIDefaults, + useSha, +} from '../../../utils/project'; + +export default async function () { + // forcibly remove in case another test doesn't clean itself up + await rimraf('node_modules/@angular/ssr'); + + await ng('generate', 'app', 'test-project-two', '--no-standalone', '--skip-install'); + await ng('generate', 'private-e2e', '--related-app-name=test-project-two'); + + // Setup testing to use CI Chrome. + await useCIChrome('test-project-two', 'projects/test-project-two/e2e/'); + await useCIDefaults('test-project-two'); + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + + if (useWebpackBuilder) { + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project-two']['architect']['build']; + build.builder = '@angular-devkit/build-angular:browser'; + build.options = { + ...build.options, + main: build.options.browser, + browser: undefined, + index: 'src/index.html', + }; + + build.configurations.development = { + ...build.configurations.development, + vendorChunk: true, + namedChunks: true, + buildOptimizer: false, + }; + }); + } + + await ng( + 'add', + '@angular/ssr', + '--skip-confirmation', + '--skip-install', + '--project=test-project-two', + ); + + await useSha(); + await installWorkspacePackages(); + + if (!useWebpackBuilder) { + // Disable prerendering + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project-two']['architect']['build']; + build.configurations.production.prerender = false; + build.options.outputMode = undefined; + }); + + await updateServerFileForEsbuild('projects/test-project-two/src/server.ts'); + } + + await writeMultipleFiles({ + 'projects/test-project-two/src/app/app.css': `div { color: #000 }`, + 'projects/test-project-two/src/styles.css': `* { color: #000 }`, + 'projects/test-project-two/src/main.ts': ` + import { platformBrowser } from '@angular/platform-browser'; + import { AppModule } from './app/app-module'; + + (window as any)['doBootstrap'] = () => { + platformBrowser() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); + }; + `, + 'projects/test-project-two/e2e/src/app.e2e-spec.ts': ` + import { browser, by, element } from 'protractor'; + import * as webdriver from 'selenium-webdriver'; + + function verifyNoBrowserErrors() { + return browser + .manage() + .logs() + .get('browser') + .then(function (browserLog: any[]) { + const errors: any[] = []; + browserLog.filter((logEntry) => { + const msg = logEntry.message; + console.log('>> ' + msg); + if (logEntry.level.value >= webdriver.logging.Level.INFO.value) { + errors.push(msg); + } + }); + expect(errors).toEqual([]); + }); + } + + describe('Hello world E2E Tests', () => { + beforeAll(async () => { + await browser.waitForAngularEnabled(false); + }); + + it('should display: Welcome', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + const style = await browser.driver.findElement(by.css('style[ng-app-id="ng"]')); + expect(await style.getText()).not.toBeNull(); + + // Test the contents from the server. + const serverDiv = await browser.driver.findElement(by.css('h1')); + expect(await serverDiv.getText()).toMatch('Hello'); + + // Bootstrap the client side app. + await browser.executeScript('doBootstrap()'); + + // Retest the contents after the client bootstraps. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Make sure the server styles got replaced by client side ones. + expect(await element(by.css('style[ng-app-id="ng"]')).isPresent()).toBeFalsy(); + expect(await element(by.css('style')).getText()).toMatch(''); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + + it('stylesheets should be configured to load asynchronously', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + const styleTag = await browser.driver.findElement(by.css('link[rel="stylesheet"]')); + expect(await styleTag.getAttribute('media')).toMatch('all'); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + }); + `, + }); + + async function spawnServer(): Promise { + const port = await findFreePort(); + + const runCommand = useWebpackBuilder ? 'serve:ssr' : `serve:ssr:test-project-two`; + + await execAndWaitForOutputToMatch( + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; + } + + await ng('build', 'test-project-two'); + + if (useWebpackBuilder) { + // Build server code + await ng('run', `test-project-two:server`); + } + + const port = await spawnServer(); + await ng( + 'e2e', + '--project=test-project-two', + `--base-url=http://localhost:${port}`, + '--dev-server-target=', + ); +} diff --git a/tests/e2e/tests/build/server-rendering/express-engine-standalone.ts b/tests/e2e/tests/build/server-rendering/express-engine-standalone.ts new file mode 100644 index 000000000000..7c819e67693a --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/express-engine-standalone.ts @@ -0,0 +1,131 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { updateJsonFile, updateServerFileForEsbuild, useSha } from '../../../utils/project'; + +export default async function () { + // forcibly remove in case another test doesn't clean itself up + await rimraf('node_modules/@angular/ssr'); + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + + if (!useWebpackBuilder) { + // Disable prerendering + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.options.outputMode = undefined; + }); + + await updateServerFileForEsbuild('src/server.ts'); + } + + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + 'src/app/app.css': `div { color: #000 }`, + 'src/styles.css': `* { color: #000 }`, + 'src/main.ts': `import { bootstrapApplication } from '@angular/platform-browser'; + import { App } from './app/app'; + import { appConfig } from './app/app.config'; + + (window as any)['doBootstrap'] = () => { + bootstrapApplication(App, appConfig).catch((err) => console.error(err)); + }; + `, + 'e2e/src/app.e2e-spec.ts': ` + import { browser, by, element } from 'protractor'; + import * as webdriver from 'selenium-webdriver'; + + function verifyNoBrowserErrors() { + return browser + .manage() + .logs() + .get('browser') + .then(function (browserLog: any[]) { + const errors: any[] = []; + browserLog.filter((logEntry) => { + const msg = logEntry.message; + console.log('>> ' + msg); + if (logEntry.level.value >= webdriver.logging.Level.INFO.value) { + errors.push(msg); + } + }); + expect(errors).toEqual([]); + }); + } + + describe('Hello world E2E Tests', () => { + beforeAll(async () => { + await browser.waitForAngularEnabled(false); + }); + + it('should display: Welcome', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + const style = await browser.driver.findElement(by.css('style[ng-app-id="ng"]')); + expect(await style.getText()).not.toBeNull(); + + // Test the contents from the server. + const serverDiv = await browser.driver.findElement(by.css('h1')); + expect(await serverDiv.getText()).toMatch('Hello'); + + // Bootstrap the client side app. + await browser.executeScript('doBootstrap()'); + + // Retest the contents after the client bootstraps. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Make sure the server styles got replaced by client side ones. + expect(await element(by.css('style[ng-app-id="ng"]')).isPresent()).toBeFalsy(); + expect(await element(by.css('style')).getText()).toMatch(''); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + + it('stylesheets should be configured to load asynchronously', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + const styleTag = await browser.driver.findElement(by.css('link[rel="stylesheet"]')); + expect(await styleTag.getAttribute('media')).toMatch('all'); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + }); + `, + }); + + async function spawnServer(): Promise { + const port = await findFreePort(); + const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; + + await execAndWaitForOutputToMatch( + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; + } + + await ng('build'); + if (useWebpackBuilder) { + // Build server code + await ng('run', `test-project:server`); + } + + const port = await spawnServer(); + await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts new file mode 100644 index 000000000000..52ceafa7b05f --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-external-dependencies.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert'; +import { ng } from '../../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { updateJsonFile, useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.options.externalDependencies = [ + '@angular/platform-browser', + '@angular/core', + '@angular/router', + '@angular/common', + '@angular/common/http', + '@angular/platform-browser/animations', + ]; + }); + + await ng('build'); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts new file mode 100644 index 000000000000..6d0a45459a16 --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -0,0 +1,118 @@ +import { join } from 'node:path'; +import assert from 'node:assert'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { langTranslations, setupI18nConfig } from '../../i18n/setup'; +import { findFreePort } from '../../../utils/network'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Setup project + await setupI18nConfig(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Ssr } from './ssr/ssr'; + import { Ssg } from './ssg/ssg'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: 'ssr', + component: Ssr, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: '', + renderMode: RenderMode.Prerender, + }, + { + path: 'ssg', + renderMode: RenderMode.Prerender, + }, + { + path: '**', + renderMode: RenderMode.Server, + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg', 'csr', 'ssr']; + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + await noSilentNg('build', '--output-mode=server', '--base-href=/base/'); + + for (const { lang, outputPath } of langTranslations) { + await expectFileToMatch(join(outputPath, 'index.html'), `

${lang}

`); + await expectFileToMatch(join(outputPath, 'ssg/index.html'), `

${lang}

`); + } + + // Tests responses + const port = await spawnServer(); + const pathnamesToVerify = ['/ssr', '/ssg']; + for (const { lang } of langTranslations) { + for (const pathname of pathnamesToVerify) { + const res = await fetch(`http://localhost:${port}/base/${lang}${pathname}`); + const text = await res.text(); + + assert.match( + text, + new RegExp(`

${lang}

`), + `Response for '${lang}${pathname}': '

${lang}

' was not matched in content.`, + ); + } + } +} + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts new file mode 100644 index 000000000000..79fc755c4477 --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts @@ -0,0 +1,153 @@ +import { join } from 'node:path'; +import assert from 'node:assert'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { langTranslations, setupI18nConfig } from '../../i18n/setup'; +import { findFreePort } from '../../../utils/network'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { updateJsonFile, useSha } from '../../../utils/project'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Setup project + await setupI18nConfig(); + + // Update angular.json + const URL_SUB_PATH: Record = { + 'en-US': '', + 'fr': 'fr', + 'de': 'deutsche', + }; + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const i18n: Record = appProject.i18n; + i18n.sourceLocale = { + subPath: URL_SUB_PATH['en-US'], + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + subPath: URL_SUB_PATH['fr'], + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + subPath: URL_SUB_PATH['de'], + }; + }); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Ssr } from './ssr/ssr'; + import { Ssg } from './ssg/ssg'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: 'ssr', + component: Ssr, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: '', + renderMode: RenderMode.Prerender, + }, + { + path: 'ssg', + renderMode: RenderMode.Prerender, + }, + { + path: '**', + renderMode: RenderMode.Server, + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg', 'ssr']; + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + await noSilentNg('build', '--output-mode=server', '--base-href=/base/'); + + const pathToVerify = ['/index.html', '/ssg/index.html']; + for (const { lang } of langTranslations) { + const subPath = URL_SUB_PATH[lang]; + const outputPath = join('dist/test-project/browser', subPath); + + for (const path of pathToVerify) { + await expectFileToMatch(join(outputPath, path), `

${lang}

`); + const baseHref = `/base/${subPath ? `${subPath}/` : ''}`; + await expectFileToMatch(join(outputPath, path), ``); + } + } + + // Tests responses + const port = await spawnServer(); + const pathnamesToVerify = ['/ssr', '/ssg']; + + for (const { lang } of langTranslations) { + for (const pathname of pathnamesToVerify) { + const subPath = URL_SUB_PATH[lang]; + const urlPathname = `/base${subPath ? `/${subPath}` : ''}${pathname}`; + const res = await fetch(`http://localhost:${port}${urlPathname}`); + const text = await res.text(); + + assert.match( + text, + new RegExp(`

${lang}

`), + `Response for '${urlPathname}': '

${lang}

' was not matched in content.`, + ); + } + } +} + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts new file mode 100644 index 000000000000..994d77343d1e --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts @@ -0,0 +1,129 @@ +import { join } from 'node:path'; +import assert from 'node:assert'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { langTranslations, setupI18nConfig } from '../../i18n/setup'; +import { findFreePort } from '../../../utils/network'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Setup project + await setupI18nConfig(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Ssr } from './ssr/ssr'; + import { Ssg } from './ssg/ssg'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: 'ssr', + component: Ssr, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: '', + renderMode: RenderMode.Prerender, + }, + { + path: 'ssg', + renderMode: RenderMode.Prerender, + }, + { + path: '**', + renderMode: RenderMode.Server, + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg', 'ssr']; + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + await noSilentNg('build', '--output-mode=server'); + + const expects: Record = { + 'index.html': 'home works!', + 'ssg/index.html': 'ssg works!', + }; + + for (const { lang, outputPath } of langTranslations) { + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join(outputPath, filePath), `

${lang}

`); + await expectFileToMatch(join(outputPath, filePath), fileMatch); + } + } + + // Tests responses + const port = await spawnServer(); + const pathname = '/ssr'; + + // We run the tests twice to ensure that the locale ID is set correctly. + for (const iteration of [1, 2]) { + for (const { lang, translation } of langTranslations) { + const res = await fetch(`http://localhost:${port}/${lang}${pathname}`); + const text = await res.text(); + + for (const match of [`

${translation.date}

`, `

${lang}

`]) { + assert.match( + text, + new RegExp(match), + `Response for '${lang}${pathname}': '${match}' was not matched in content. Iteration: ${iteration}.`, + ); + } + } + } +} + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts new file mode 100644 index 000000000000..130ade10ba9f --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts @@ -0,0 +1,132 @@ +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import assert from 'node:assert'; +import { expectFileToMatch, writeMultipleFiles } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { + installPackage, + installWorkspacePackages, + uninstallPackage, +} from '../../../utils/packages'; +import { updateJsonFile, useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { findFreePort } from '../../../utils/network'; +import { readFile } from 'node:fs/promises'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await installPackage('h3@1'); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Ssr } from './ssr/ssr'; + import { SsgWithParams } from './ssg-with-params/ssg-with-params'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssr', + component: Ssr, + }, + { + path: 'ssg/:id', + component: SsgWithParams, + }, + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: 'ssg/:id', + renderMode: RenderMode.Prerender, + getPrerenderParams: async() => [{id: 'one'}, {id: 'two'}], + }, + { + path: 'ssr', + renderMode: RenderMode.Server, + }, + { + path: '**', + renderMode: RenderMode.Prerender, + }, + ]; + `, + 'src/server.ts': ` + import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; + import { createApp, createRouter, toWebHandler, defineEventHandler, toWebRequest } from 'h3'; + + export const app = createApp(); + const router = createRouter(); + const angularAppEngine = new AngularAppEngine(); + + router.use( + '/**', + defineEventHandler((event) => angularAppEngine.handle(toWebRequest(event))), + ); + + app.use(router); + export const reqHandler = createRequestHandler(toWebHandler(app)); + `, + }); + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssr', 'ssg-with-params']; + + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + await updateJsonFile('angular.json', (json) => { + const buildTarget = json['projects']['test-project']['architect']['build']; + const options = buildTarget['options']; + options['ssr']['experimentalPlatform'] = 'neutral'; + options['outputMode'] = 'server'; + }); + + await noSilentNg('build'); + + // Valid SSG pages work + const expects: Record = { + 'index.html': 'home works!', + 'ssg/one/index.html': 'ssg-with-params works!', + 'ssg/two/index.html': 'ssg-with-params works!', + }; + + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch); + } + + const filesDoNotExist: string[] = ['csr/index.html', 'ssr/index.html', 'redirect/index.html']; + for (const filePath of filesDoNotExist) { + const file = join('dist/test-project/browser', filePath); + assert.equal(existsSync(file), false, `Expected '${file}' to not exist.`); + } + + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npx', + ['-y', 'listhen@1', './dist/test-project/server/server.mjs', `--port=${port}`], + /Server initialized/, + ); + + const res = await fetch(`http://localhost:${port}/ssr`); + const text = await res.text(); + assert.match(text, new RegExp('ssr works!')); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts new file mode 100644 index 000000000000..5205d20eeb0a --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -0,0 +1,213 @@ +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import assert from 'node:assert'; +import { expectFileToMatch, readFile, replaceInFile, writeFile } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { findFreePort } from '../../../utils/network'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Test scenario to verify that the content length, including \r\n, is accurate + await replaceInFile('src/app/app.ts', "title = signal('", "title = signal('Title\\r\\n"); + + // Ensure text has been updated. + assert.match(await readFile('src/app/app.ts'), /title = signal\('Title/); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Csr } from './csr/csr'; + import { Ssr } from './ssr/ssr'; + import { Ssg } from './ssg/ssg'; + import { SsgWithParams } from './ssg-with-params/ssg-with-params'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: 'ssr', + component: Ssr, + }, + { + path: 'csr', + component: Csr, + }, + { + path: 'redirect', + redirectTo: 'ssg' + }, + { + path: 'ssg/:id', + component: SsgWithParams, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: 'ssg/:id', + renderMode: RenderMode.Prerender, + headers: { 'x-custom': 'ssg-with-params' }, + getPrerenderParams: async() => [{id: 'one'}, {id: 'two'}], + }, + { + path: 'ssr', + renderMode: RenderMode.Server, + headers: { 'x-custom': 'ssr' }, + }, + { + path: 'csr', + renderMode: RenderMode.Client, + headers: { 'x-custom': 'csr' }, + }, + { + path: '**', + renderMode: RenderMode.Prerender, + headers: { 'x-custom': 'ssg' }, + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg', 'ssg-with-params', 'csr', 'ssr']; + + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + // Generate app-shell + await ng('g', 'app-shell'); + + await noSilentNg('build', '--output-mode=server'); + + const expects: Record = { + 'index.html': 'home works!', + 'ssg/index.html': 'ssg works!', + 'ssg/one/index.html': 'ssg-with-params works!', + 'ssg/two/index.html': 'ssg-with-params works!', + }; + + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch); + } + + const filesDoNotExist: string[] = ['csr/index.html', 'ssr/index.html', 'redirect/index.html']; + for (const filePath of filesDoNotExist) { + const file = join('dist/test-project/browser', filePath); + assert.equal(existsSync(file), false, `Expected '${file}' to not exist.`); + } + + // Tests responses + const responseExpects: Record< + string, + { headers: Record; content: string; serverContext: string } + > = { + '/': { + content: 'home works', + serverContext: 'ng-server-context="ssg"', + headers: { + 'x-custom': 'ssg', + }, + }, + '/ssg': { + content: 'ssg works!', + serverContext: 'ng-server-context="ssg"', + headers: { + 'x-custom': 'ssg', + }, + }, + '/ssr': { + content: 'ssr works!', + serverContext: 'ng-server-context="ssr"', + headers: { + 'x-custom': 'ssr', + }, + }, + '/csr': { + content: 'app-shell works', + serverContext: 'ng-server-context="ssg"', + headers: { + 'x-custom': 'csr', + }, + }, + '/redirect': { + content: 'ssg works!', + serverContext: 'ng-server-context="ssg"', + headers: { + 'x-custom': 'ssg', + }, + }, + }; + + const port = await spawnServer(); + for (const [pathname, { content, headers, serverContext }] of Object.entries(responseExpects)) { + // NOTE: A global 'UND_ERR_SOCKET' may occur due to an incorrect Content-Length header value. + const res = await fetch(`http://localhost:${port}${pathname}`); + const text = await res.text(); + + assert.match( + text, + new RegExp(content), + `Response for '${pathname}': ${content} was not matched in content.`, + ); + + assert.match( + text, + new RegExp(serverContext), + `Response for '${pathname}': ${serverContext} was not matched in content.`, + ); + + for (const [name, value] of Object.entries(headers)) { + assert.equal( + res.headers.get(name), + value, + `Response for '${pathname}': ${name} header value did not match expected.`, + ); + } + } +} + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts new file mode 100644 index 000000000000..b565144b37bf --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts @@ -0,0 +1,130 @@ +import assert, { match } from 'node:assert'; +import { readFile, writeMultipleFiles } from '../../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../../utils/process'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Add asset + 'public/media.json': JSON.stringify({ dataFromAssets: true }), + // Update component to do an HTTP call to asset and API. + 'src/app/app.ts': ` + import { ChangeDetectorRef, Component, inject } from '@angular/core'; + import { JsonPipe } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + import { HttpClient } from '@angular/common/http'; + + @Component({ + selector: 'app-root', + imports: [JsonPipe, RouterOutlet], + template: \` +

{{ assetsData | json }}

+

{{ apiData | json }}

+ + \`, + }) + export class App { + assetsData: any; + apiData: any; + private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + const http = inject(HttpClient); + + http.get('/media.json').toPromise().then((d) => { + this.assetsData = d; + this.cdr.markForCheck(); + }); + + http.get('/api').toPromise().then((d) => { + this.apiData = d; + this.cdr.markForCheck(); + }); + } + } + `, + // Add http client and route + 'src/app/app.config.ts': ` + import { ApplicationConfig } from '@angular/core'; + import { provideRouter } from '@angular/router'; + + import { Home } from './home/home'; + import { provideClientHydration } from '@angular/platform-browser'; + import { provideHttpClient, withFetch } from '@angular/common/http'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideRouter([{ + path: 'home', + component: Home, + }]), + provideClientHydration(), + provideHttpClient(withFetch()), + ], + }; + `, + 'src/server.ts': ` + import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; + import express from 'express'; + import { join } from 'node:path'; + + export function app(): express.Express { + const server = express(); + const browserDistFolder = join(import.meta.dirname, '../browser'); + const angularNodeAppEngine = new AngularNodeAppEngine(); + + server.get('/api', (req, res) => { + res.json({ dataFromAPI: true }) + }); + + server.use(express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html' + })); + + server.use((req, res, next) => { + angularNodeAppEngine.handle(req) + .then((response) => response ? writeResponseToNodeResponse(response, res) : next()) + .catch(next); + }); + return server; + } + + const server = app(); + + if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000; + server.listen(port, (error) => { + if (error) { + throw error; + } + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + export const reqHandler = createNodeRequestHandler(server); + `, + }); + + await silentNg('generate', 'component', 'home'); + + await noSilentNg('build', '--output-mode=static'); + + const contents = await readFile('dist/test-project/browser/home/index.html'); + match(contents, /

{[\S\s]*"dataFromAssets":[\s\S]*true[\S\s]*}<\/p>/); + match(contents, /

{[\S\s]*"dataFromAPI":[\s\S]*true[\S\s]*}<\/p>/); + match(contents, /home works!/); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts new file mode 100644 index 000000000000..245375101946 --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static-i18n_APP_BASE_HREF.ts @@ -0,0 +1,105 @@ +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import assert from 'node:assert'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { langTranslations, setupI18nConfig } from '../../i18n/setup'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Setup project + await setupI18nConfig(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + import { Ssg } from './ssg/ssg'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: '**', + component: Home, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender, + }, + ]; + `, + ); + + await writeFile( + 'src/app/app.config.ts', + ` + import { ApplicationConfig } from '@angular/core'; + import { provideRouter } from '@angular/router'; + + import { routes } from './app.routes'; + import { provideClientHydration } from '@angular/platform-browser'; + import { APP_BASE_HREF } from '@angular/common'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + provideClientHydration(), + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], + }; + `, + ); + + // Generate components for the above routes + await silentNg('generate', 'component', 'home'); + await silentNg('generate', 'component', 'ssg'); + + await noSilentNg('build', '--output-mode=static'); + + for (const { lang, outputPath } of langTranslations) { + await expectFileToMatch(join(outputPath, 'index.html'), `

${lang}

`); + await expectFileToMatch(join(outputPath, 'ssg/index.html'), `

${lang}

`); + } + + // Check that server directory does not exist + assert( + !existsSync('dist/test-project/server'), + 'Server directory should not exist when output-mode is static', + ); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts new file mode 100644 index 000000000000..77f954be4f4d --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts @@ -0,0 +1,134 @@ +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import assert from 'node:assert'; +import { + expectFileNotToExist, + expectFileToMatch, + replaceInFile, + writeFile, +} from '../../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Add routes + await writeFile( + 'src/app/app.routes.ts', + ` + import { inject } from '@angular/core'; + import { Routes, Router } from '@angular/router'; + import { Home } from './home/home'; + import { Ssg } from './ssg/ssg'; + import { SsgWithParams } from './ssg-with-params/ssg-with-params'; + + export const routes: Routes = [ + { + path: '', + component: Home, + }, + { + path: 'ssg', + component: Ssg, + }, + { + path: 'ssg-redirect', + redirectTo: 'ssg' + }, + { + path: 'ssg-redirect-via-guard', + canActivate: [() => { + return inject(Router).createUrlTree(['ssg'], { queryParams: { foo: 'bar' }}) + }], + }, + { + path: 'ssg/:id', + component: SsgWithParams, + }, + { + path: '**', + component: Home, + }, + ]; + `, + ); + + // Add server routing + await writeFile( + 'src/app/app.routes.server.ts', + ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: 'ssg/:id', + renderMode: RenderMode.Prerender, + getPrerenderParams: async() => [{id: 'one'}, {id: 'two'}], + }, + { + path: '**', + renderMode: RenderMode.Server, + }, + ]; + `, + ); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg', 'ssg-with-params']; + + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + // Should error as above we set `RenderMode.Server` + const { message: errorMessage } = await expectToFail(() => + noSilentNg('build', '--output-mode=static'), + ); + assert.match( + errorMessage, + new RegExp( + `Route '/' is configured with server render mode, but the build 'outputMode' is set to 'static'.`, + ), + ); + + // Fix the error + await replaceInFile('src/app/app.routes.server.ts', 'RenderMode.Server', 'RenderMode.Prerender'); + await noSilentNg('build', '--output-mode=static'); + + const expects: Record = { + 'index.html': /ng-server-context="ssg".+home works!/, + 'ssg/index.html': /ng-server-context="ssg".+ssg works!/, + 'ssg/one/index.html': /ng-server-context="ssg".+ssg-with-params works!/, + 'ssg/two/index.html': /ng-server-context="ssg".+ssg-with-params works!/, + // When static redirects are generated as meta tags. + 'ssg-redirect/index.html': '', + 'ssg-redirect-via-guard/index.html': + '', + }; + + for (const [filePath, fileMatch] of Object.entries(expects)) { + await expectFileToMatch(join('dist/test-project/browser', filePath), fileMatch); + } + + // Check that server directory does not exist + assert( + !existsSync('dist/test-project/server'), + 'Server directory should not exist when output-mode is static', + ); + + // Should not prerender the catch all + await expectFileNotToExist(join('dist/test-project/browser/**/index.html')); +} diff --git a/tests/e2e/tests/build/server-rendering/server-routes-preload-links.ts b/tests/e2e/tests/build/server-rendering/server-routes-preload-links.ts new file mode 100644 index 000000000000..f1437392492d --- /dev/null +++ b/tests/e2e/tests/build/server-rendering/server-routes-preload-links.ts @@ -0,0 +1,203 @@ +import assert from 'node:assert'; +import { replaceInFile, writeMultipleFiles } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ngServe, updateJsonFile, useSha } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { findFreePort } from '../../../utils/network'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + appProject.architect['build'].options.namedChunks = true; + }); + + // Add routes + await writeMultipleFiles({ + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + + export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./home/home').then(c => c.Home), + }, + { + path: 'ssg', + loadChildren: () => import('./ssg.routes').then(m => m.routes), + }, + { + path: 'ssr', + loadComponent: () => import('./ssr/ssr').then(c => c.Ssr), + }, + { + path: 'csr', + loadComponent: () => import('./csr/csr').then(c => c.Csr), + }, + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { + path: 'ssr', + renderMode: RenderMode.Server, + }, + { + path: 'csr', + renderMode: RenderMode.Client, + }, + { + path: '**', + renderMode: RenderMode.Prerender, + }, + ]; + `, + 'src/app/cross-dep.ts': `export const foo = 'foo';`, + 'src/app/ssg.routes.ts': ` + import { Routes } from '@angular/router'; + + export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./ssg-component/ssg-component').then(c => c.SsgComponent), + }, + { + path: 'one', + loadComponent: () => import('./ssg-one/ssg-one').then(c => c.SsgOne), + }, + { + path: 'two', + loadComponent: () => import('./ssg-two/ssg-two').then(c => c.SsgTwo), + }, + ];`, + }); + + // Generate components for the above routes + const componentNames: string[] = ['home', 'ssg-component', 'csr', 'ssr', 'ssg-one', 'ssg-two']; + + for (const componentName of componentNames) { + await silentNg('generate', 'component', componentName); + } + + // Add a cross-dependency + await Promise.all([ + replaceInFile( + 'src/app/ssg-one/ssg-one.ts', + `One {`, + `One { + async ngOnInit() { + await import('../cross-dep'); + } + `, + ), + replaceInFile( + 'src/app/ssg-two/ssg-two.ts', + `Two {`, + `Two { + async ngOnInit() { + await import('../cross-dep'); + } + `, + ), + ]); + + // Test both vite and `ng build` + await runTests(await ngServe()); + + await noSilentNg('build', '--output-mode=server'); + await runTests(await spawnServer()); +} + +const RESPONSE_EXPECTS: Record< + string, + { + matches: RegExp[]; + notMatches: RegExp[]; + } +> = { + '/': { + matches: [//], + notMatches: [/ssg\-component/, /ssr/, /csr/, /cross-dep-/], + }, + '/ssg': { + matches: [ + //, + //, + ], + notMatches: [/home/, /ssr/, /csr/, /ssg-one/, /ssg-two/, /cross-dep-/], + }, + '/ssg/one': { + matches: [ + //, + //, + ], + notMatches: [/home/, /ssr/, /csr/, /ssg-two/, /ssg\-component/, /cross-dep-/], + }, + '/ssg/two': { + matches: [ + //, + //, + ], + notMatches: [/home/, /ssr/, /csr/, /ssg-one/, /ssg\-component/, /cross-dep-/], + }, + '/ssr': { + matches: [//], + notMatches: [/home/, /ssg\-component/, /csr/], + }, + '/csr': { + matches: [//], + notMatches: [/home/, /ssg\-component/, /ssr/, /cross-dep-/], + }, +}; + +async function runTests(port: number): Promise { + for (const [pathname, { matches, notMatches }] of Object.entries(RESPONSE_EXPECTS)) { + const res = await fetch(`http://localhost:${port}${pathname}`); + const text = await res.text(); + + for (const match of matches) { + assert.match(text, match, `Response for '${pathname}': ${match} was not matched in content.`); + + // Ensure that the url is correct and it's a 200. + const link = text.match(match)?.[1]; + const preloadRes = await fetch(`http://localhost:${port}/${link}`); + assert.equal(preloadRes.status, 200); + } + + for (const match of notMatches) { + assert.doesNotMatch( + text, + match, + `Response for '${pathname}': ${match} was matched in content.`, + ); + } + } +} + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + ...process.env, + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/e2e/tests/build/sourcemap.ts b/tests/e2e/tests/build/sourcemap.ts new file mode 100644 index 000000000000..2e153e637f30 --- /dev/null +++ b/tests/e2e/tests/build/sourcemap.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + + // The below is needed to cache bundles and verify that sourcemaps are generated + // corretly when output-hashing is disabled. + await ng('build', '--output-hashing=bundles', '--source-map', '--configuration=development'); + + await ng('build', '--output-hashing=none', '--source-map'); + await testForSourceMaps(useWebpackBuilder ? 2 : 1); + + await ng('build', '--output-hashing=none', '--source-map', '--configuration=development'); + await testForSourceMaps(useWebpackBuilder ? 3 : 1); +} + +async function testForSourceMaps(expectedNumberOfFiles: number): Promise { + await expectFileToExist('dist/test-project/browser/main.js.map'); + + const files = fs.readdirSync('./dist/test-project/browser'); + + let count = 0; + for (const file of files) { + if (!file.endsWith('.js')) { + continue; + } + + ++count; + + assert(files.includes(file + '.map'), 'Sourcemap not generated for ' + file); + + const content = fs.readFileSync('./dist/test-project/browser/' + file, 'utf8'); + let lastLineIndex = content.lastIndexOf('\n'); + if (lastLineIndex === content.length - 1) { + // Skip empty last line + lastLineIndex = content.lastIndexOf('\n', lastLineIndex - 1); + } + const comment = lastLineIndex !== -1 && content.slice(lastLineIndex).trim(); + assert.equal( + comment, + `//# sourceMappingURL=${file}.map`, + 'Sourcemap comment not generated for ' + file, + ); + } + + assert( + count >= expectedNumberOfFiles, + `Javascript file count is low. Expected ${expectedNumberOfFiles} but found ${count}`, + ); +} diff --git a/tests/e2e/tests/build/styles/bootstrap.ts b/tests/e2e/tests/build/styles/bootstrap.ts new file mode 100644 index 000000000000..ec8c9f7936be --- /dev/null +++ b/tests/e2e/tests/build/styles/bootstrap.ts @@ -0,0 +1,22 @@ +import { writeMultipleFiles } from '../../../utils/fs'; +import { installPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + // Install bootstrap + await installPackage('bootstrap@5'); + + await writeMultipleFiles({ + 'src/styles.scss': ` + @import 'bootstrap/scss/bootstrap'; + `, + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.scss' }]; + }); + + await ng('build'); +} diff --git a/tests/e2e/tests/build/styles/include-paths.ts b/tests/e2e/tests/build/styles/include-paths.ts new file mode 100644 index 000000000000..fb1a0326ed9f --- /dev/null +++ b/tests/e2e/tests/build/styles/include-paths.ts @@ -0,0 +1,60 @@ +import { writeMultipleFiles, expectFileToMatch, replaceInFile, createDir } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + await createDir('src/style-paths'); + await writeMultipleFiles({ + 'src/style-paths/_variables.scss': '$primary-color: red;', + 'src/styles.scss': ` + @import 'variables'; + h1 { color: $primary-color; } + `, + 'src/app/app.scss': ` + @import 'variables'; + h2 { color: $primary-color; } + `, + 'src/style-paths/variables.less': '@primary-color: #ADDADD;', + 'src/styles.less': ` + @import 'variables'; + h5 { color: @primary-color; } + `, + 'src/app/app.less': ` + @import 'variables'; + h6 { color: @primary-color; } + `, + }); + + await replaceInFile( + 'src/app/app.ts', + `styleUrl: './app.css\'`, + `styleUrls: ['./app.scss', './app.less']`, + ); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [ + { input: 'src/styles.scss' }, + { input: 'src/styles.less' }, + ]; + appArchitect.build.options.stylePreprocessorOptions = { + includePaths: ['src/style-paths'], + }; + }); + + await ng('build', '--configuration=development'); + await expectFileToMatch('dist/test-project/browser/styles.css', /h1\s*{\s*color: red;\s*}/); + await expectFileToMatch('dist/test-project/browser/main.js', /h2.*{.*color: red;.*}/); + // These checks are for the less files + await expectFileToMatch('dist/test-project/browser/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/); + await expectFileToMatch('dist/test-project/browser/main.js', /h6.*{.*color: #ADDADD;.*}/); + + await ng('build', '--no-aot', '--configuration=development'); + await expectFileToMatch('dist/test-project/browser/styles.css', /h1\s*{\s*color: red;\s*}/); + await expectFileToMatch('dist/test-project/browser/main.js', /h2.*{[\S\s]*color: red;[\S\s]*}/); + await expectFileToMatch('dist/test-project/browser/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /h6.*{[\S\s]*color: #ADDADD;[\S\s]*}/, + ); +} diff --git a/tests/e2e/tests/build/styles/less.ts b/tests/e2e/tests/build/styles/less.ts new file mode 100644 index 000000000000..c5d58d2d3b08 --- /dev/null +++ b/tests/e2e/tests/build/styles/less.ts @@ -0,0 +1,60 @@ +import { + writeMultipleFiles, + deleteFile, + expectFileToMatch, + replaceInFile, +} from '../../../utils/fs'; +import { expectToFail } from '../../../utils/utils'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + return writeMultipleFiles({ + 'src/styles.less': ` + @import './imported-styles.less'; + body { background-color: blue; } + `, + 'src/imported-styles.less': 'p { background-color: red; }', + 'src/app/app.less': ` + .outer { + .inner { + background: #fff; + } + } + `, + }) + .then(() => deleteFile('src/app/app.css')) + .then(() => + updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.less' }]; + }), + ) + .then(() => replaceInFile('src/app/app.ts', './app.css', './app.less')) + .then(() => ng('build', '--source-map', '--configuration=development')) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /body\s*{\s*background-color: blue;\s*}/, + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /p\s*{\s*background-color: red;\s*}/, + ), + ) + .then(() => + expectToFail(() => + expectFileToMatch('dist/test-project/browser/styles.css', '"mappings":""'), + ), + ) + .then(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /.outer.*.inner.*background:\s*#[fF]+/, + ), + ); +} diff --git a/tests/e2e/tests/build/styles/loaders.ts b/tests/e2e/tests/build/styles/loaders.ts new file mode 100644 index 000000000000..cbb602ece07b --- /dev/null +++ b/tests/e2e/tests/build/styles/loaders.ts @@ -0,0 +1,43 @@ +import { + writeMultipleFiles, + deleteFile, + expectFileToMatch, + replaceInFile, +} from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await writeMultipleFiles({ + 'src/styles.scss': ` + @import './imported-styles.scss'; + body { background-color: blue; } + `, + 'src/imported-styles.scss': 'p { background-color: red; }', + 'src/app/app.scss': ` + .outer { + .inner { + background: #fff; + } + } + `, + }); + + await deleteFile('src/app/app.css'); + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.scss' }]; + }); + await replaceInFile('src/app/app.ts', './app.css', './app.scss'); + + await ng('build', '--configuration=development'); + + await expectToFail(() => expectFileToMatch('dist/test-project/browser/styles.css', /exports/)); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /".*module\.exports.*\.outer.*background:/, + ), + ); +} diff --git a/tests/e2e/tests/build/styles/sass-pkg-importer.ts b/tests/e2e/tests/build/styles/sass-pkg-importer.ts new file mode 100644 index 000000000000..8fbb6e74310f --- /dev/null +++ b/tests/e2e/tests/build/styles/sass-pkg-importer.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert'; +import { writeFile } from '../../../utils/fs'; +import { getActivePackageManager, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { isPrereleaseCli, updateJsonFile } from '../../../utils/project'; +import { appendFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // forcibly remove in case another test doesn't clean itself up + await uninstallPackage('@angular/material'); + + const isPrerelease = await isPrereleaseCli(); + const tag = isPrerelease ? '@next' : ''; + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); + await Promise.all([ + updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = ['src/styles.scss']; + }), + writeFile('src/styles.scss', `@use 'pkg:@angular/material' as mat;`), + ]); + + await ng('build'); +} diff --git a/tests/e2e/tests/build/styles/sass.ts b/tests/e2e/tests/build/styles/sass.ts new file mode 100644 index 000000000000..f4259118317d --- /dev/null +++ b/tests/e2e/tests/build/styles/sass.ts @@ -0,0 +1,54 @@ +import { + writeMultipleFiles, + deleteFile, + expectFileToMatch, + replaceInFile, +} from '../../../utils/fs'; +import { expectToFail } from '../../../utils/utils'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + await writeMultipleFiles({ + 'src/styles.sass': ` + @import './imported-styles.sass' + body + background-color: blue + `, + 'src/imported-styles.sass': ` + p + background-color: red + `, + 'src/app/app.sass': ` + .outer + .inner + background: #fff + `, + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.sass' }]; + }); + + await deleteFile('src/app/app.css'); + await replaceInFile('src/app/app.ts', './app.css', './app.sass'); + + await ng('build', '--source-map', '--configuration=development'); + + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /body\s*{\s*background-color: blue;\s*}/, + ); + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /p\s*{\s*background-color: red;\s*}/, + ); + await expectToFail(() => + expectFileToMatch('dist/test-project/browser/styles.css', '"mappings":""'), + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /.outer.*.inner.*background:\s*#[fF]+/, + ); +} diff --git a/tests/e2e/tests/build/styles/scss-partial-resolution.ts b/tests/e2e/tests/build/styles/scss-partial-resolution.ts new file mode 100644 index 000000000000..1a555b26e23b --- /dev/null +++ b/tests/e2e/tests/build/styles/scss-partial-resolution.ts @@ -0,0 +1,35 @@ +import { installPackage } from '../../../utils/packages'; +import { writeMultipleFiles, deleteFile, replaceInFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + // Supports resolving node_modules with are pointing to partial files partial files. + // @material/button/button below points to @material/button/_button.scss + // https://unpkg.com/browse/@material/button@14.0.0/_button.scss + + await installPackage('@material/button@14.0.0'); + + await writeMultipleFiles({ + 'src/styles.scss': ` + @use '@material/button/button'; + + @include button.core-styles; + `, + 'src/app/app.scss': ` + @use '@material/button/button'; + + @include button.core-styles; + `, + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = ['src/styles.scss']; + }); + + await deleteFile('src/app/app.css'); + await replaceInFile('src/app/app.ts', './app.css', './app.scss'); + + await ng('build', '--configuration=development'); +} diff --git a/tests/e2e/tests/build/styles/scss.ts b/tests/e2e/tests/build/styles/scss.ts new file mode 100644 index 000000000000..69fcc1c0f060 --- /dev/null +++ b/tests/e2e/tests/build/styles/scss.ts @@ -0,0 +1,52 @@ +import { + writeMultipleFiles, + deleteFile, + expectFileToMatch, + replaceInFile, +} from '../../../utils/fs'; +import { expectToFail } from '../../../utils/utils'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + await writeMultipleFiles({ + 'src/styles.scss': ` + @import './imported-styles.scss'; + body { background-color: blue; } + `, + 'src/imported-styles.scss': 'p { background-color: red; }', + 'src/app/app.scss': ` + .outer { + .inner { + background: #fff; + } + } + `, + }); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = [{ input: 'src/styles.scss' }]; + }); + + await deleteFile('src/app/app.css'); + await replaceInFile('src/app/app.ts', './app.css', './app.scss'); + + await ng('build', '--source-map', '--configuration=development'); + + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /body\s*{\s*background-color: blue;\s*}/, + ); + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /p\s*{\s*background-color: red;\s*}/, + ); + await expectToFail(() => + expectFileToMatch('dist/test-project/browser/styles.css', '"mappings":""'), + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /.outer.*.inner.*background:\s*#[fF]+/, + ); +} diff --git a/tests/e2e/tests/build/styles/symlinked-global.ts b/tests/e2e/tests/build/styles/symlinked-global.ts new file mode 100644 index 000000000000..b38663b321e9 --- /dev/null +++ b/tests/e2e/tests/build/styles/symlinked-global.ts @@ -0,0 +1,27 @@ +import { symlinkSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { expectFileToMatch, writeMultipleFiles } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + await writeMultipleFiles({ + 'src/styles.scss': `p { color: red }`, + 'src/styles-for-link.scss': `p { color: blue }`, + }); + + symlinkSync(resolve('src/styles-for-link.scss'), resolve('src/styles-linked.scss')); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect.build.options.styles = ['src/styles.scss', 'src/styles-linked.scss']; + }); + + await ng('build', '--configuration=development'); + await expectFileToMatch('dist/test-project/browser/styles.css', 'red'); + await expectFileToMatch('dist/test-project/browser/styles.css', 'blue'); + + await ng('build', '--preserve-symlinks', '--configuration=development'); + await expectFileToMatch('dist/test-project/browser/styles.css', 'red'); + await expectFileToMatch('dist/test-project/browser/styles.css', 'blue'); +} diff --git a/tests/e2e/tests/build/styles/tailwind-v2.ts b/tests/e2e/tests/build/styles/tailwind-v2.ts new file mode 100644 index 000000000000..f081e040dcb5 --- /dev/null +++ b/tests/e2e/tests/build/styles/tailwind-v2.ts @@ -0,0 +1,74 @@ +import { deleteFile, expectFileToMatch, writeFile } from '../../../utils/fs'; +import { installPackage, uninstallPackage } from '../../../utils/packages'; +import { ng, silentExec } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + // Temporarily turn off caching until the build cache accounts for the presence of tailwind + // and its configuration file. Otherwise cached builds without tailwind will cause test failures. + await ng('cache', 'off'); + + // Create configuration file + await silentExec('npx', 'tailwindcss@2', 'init'); + + // Add Tailwind directives to a component style + await writeFile('src/app/app.css', '@tailwind base; @tailwind components;'); + + // Add Tailwind directives to a global style + await writeFile('src/styles.css', '@tailwind base; @tailwind components;'); + + // Ensure installation warning is present + const { stderr } = await ng('build', '--configuration=development'); + if (!stderr.includes("To enable Tailwind CSS, please install the 'tailwindcss' package.")) { + throw new Error(`Expected tailwind installation warning. STDERR:\n${stderr}`); + } + + // Tailwind directives should be unprocessed with missing package + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ); + + // Install Tailwind + await installPackage('tailwindcss@2'); + + // Build should succeed and process Tailwind directives + await ng('build', '--configuration=development'); + + // Check for Tailwind output + await expectFileToMatch('dist/test-project/browser/styles.css', /::placeholder/); + await expectFileToMatch('dist/test-project/browser/main.js', /::placeholder/); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ), + ); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ), + ); + + // Remove configuration file + await deleteFile('tailwind.config.js'); + + // Ensure Tailwind is disabled when no configuration file is present + await ng('build', '--configuration=development'); + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ); + + // Uninstall Tailwind + await uninstallPackage('tailwindcss'); +} diff --git a/tests/e2e/tests/build/styles/tailwind-v3-cjs.ts b/tests/e2e/tests/build/styles/tailwind-v3-cjs.ts new file mode 100644 index 000000000000..c69f56aa7af4 --- /dev/null +++ b/tests/e2e/tests/build/styles/tailwind-v3-cjs.ts @@ -0,0 +1,40 @@ +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +import { installPackage, uninstallPackage } from '../../../utils/packages'; +import { ng, silentExec } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + // Temporarily turn off caching until the build cache accounts for the presence of tailwind + // and its configuration file. Otherwise cached builds without tailwind will cause test failures. + await ng('cache', 'off'); + + // Add type module in package.json. + await updateJsonFile('package.json', (json) => { + json['type'] = 'module'; + }); + + // Install Tailwind + await installPackage('tailwindcss@3'); + + // Create configuration file + await silentExec('npx', 'tailwindcss', 'init'); + + // Add Tailwind directives to a global style + await writeFile('src/styles.css', '@tailwind base; @tailwind components;'); + + // Build should succeed and process Tailwind directives + await ng('build', '--configuration=development'); + + // Check for Tailwind output + await expectFileToMatch('dist/test-project/browser/styles.css', /::placeholder/); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ), + ); + + // Uninstall Tailwind + await uninstallPackage('tailwindcss'); +} diff --git a/tests/e2e/tests/build/styles/tailwind-v3.ts b/tests/e2e/tests/build/styles/tailwind-v3.ts new file mode 100644 index 000000000000..97700a4c4b3e --- /dev/null +++ b/tests/e2e/tests/build/styles/tailwind-v3.ts @@ -0,0 +1,108 @@ +import { deleteFile, expectFileToMatch, rimraf, writeFile } from '../../../utils/fs'; +import { installPackage, uninstallPackage } from '../../../utils/packages'; +import { ng, silentExec } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + // Temporarily turn off caching until the build cache accounts for the presence of tailwind + // and its configuration file. Otherwise cached builds without tailwind will cause test failures. + await ng('cache', 'off'); + + // In case a previous test installed tailwindcss, clear it. + // (we don't clear node module directories between tests) + await rimraf('node_modules/tailwindcss'); + + // Create configuration file + await silentExec('npx', 'tailwindcss@3', 'init'); + + // Add Tailwind directives to a component style + await writeFile('src/app/app.css', '@tailwind base; @tailwind components;'); + + // Add Tailwind directives to a global style + await writeFile( + 'src/styles.css', + ` + @import url(https://fonts.googleapis.com/css?family=Roboto:400); + @tailwind base; + @tailwind components; + `, + ); + + // Ensure installation warning is present + const { stderr } = await ng('build', '--configuration=development'); + if (!stderr.includes("To enable Tailwind CSS, please install the 'tailwindcss' package.")) { + throw new Error('Expected tailwind installation warning'); + } + + // Tailwind directives should be unprocessed with missing package + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ); + + // Install Tailwind + await installPackage('tailwindcss@3'); + + // Build should succeed and process Tailwind directives + await ng('build', '--configuration=development'); + + // Check for Tailwind output + await expectFileToMatch('dist/test-project/browser/styles.css', /::placeholder/); + await expectFileToMatch('dist/test-project/browser/main.js', /::placeholder/); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ), + ); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ), + ); + + // Add Tailwind directives to an imported global style + await writeFile('src/tailwind.scss', '@tailwind base; @tailwind components;'); + await writeFile('src/styles.css', '@import "./tailwind.scss";'); + + // Build should succeed and process Tailwind directives + await ng('build', '--configuration=development'); + + // Check for Tailwind output + await expectFileToMatch('dist/test-project/browser/styles.css', /::placeholder/); + await expectFileToMatch('dist/test-project/browser/main.js', /::placeholder/); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ), + ); + await expectToFail(() => + expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ), + ); + + // Remove configuration file + await deleteFile('tailwind.config.js'); + + // Ensure Tailwind is disabled when no configuration file is present + await ng('build', '--configuration=development'); + await expectFileToMatch( + 'dist/test-project/browser/styles.css', + /@tailwind base;\s+@tailwind components;/, + ); + await expectFileToMatch( + 'dist/test-project/browser/main.js', + /@tailwind base;(?:\\n|\s*)@tailwind components;/, + ); + + // Uninstall Tailwind + await uninstallPackage('tailwindcss'); +} diff --git a/tests/e2e/tests/build/ts-paths.ts b/tests/e2e/tests/build/ts-paths.ts new file mode 100644 index 000000000000..76ee53e5d2b2 --- /dev/null +++ b/tests/e2e/tests/build/ts-paths.ts @@ -0,0 +1,53 @@ +import { appendToFile, createDir, replaceInFile, rimraf, writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateTsConfig } from '../../utils/project'; + +export default async function () { + await updateTsConfig((json) => { + json['compilerOptions']['baseUrl'] = './src'; + json['compilerOptions']['paths'] = { + '@shared': ['app/shared'], + '@shared/*': ['app/shared/*'], + '@root/*': ['./*'], + }; + }); + + await createDir('src/app/shared'); + await writeMultipleFiles({ + 'src/meaning-too.ts': 'export var meaning = 42;', + 'src/app/shared/meaning.ts': 'export var meaning = 42;', + 'src/app/shared/index.ts': `export * from './meaning'`, + }); + + await replaceInFile('src/main.ts', './app/app', '@root/app/app'); + await ng('build', '--configuration=development'); + + await updateTsConfig((json) => { + json['compilerOptions']['paths']['*'] = ['*', 'app/shared/*']; + }); + + await appendToFile( + 'src/app/app.ts', + ` + import { meaning } from 'app/shared/meaning'; + import { meaning as meaning2 } from '@shared'; + import { meaning as meaning3 } from '@shared/meaning'; + import { meaning as meaning4 } from 'meaning'; + import { meaning as meaning5 } from 'meaning-too'; + + // need to use imports otherwise they are ignored and + // no error is outputted, even if baseUrl/paths don't work + console.log(meaning) + console.log(meaning2) + console.log(meaning3) + console.log(meaning4) + console.log(meaning5) + `, + ); + + await ng('build', '--configuration=development'); + + // Simulate no package.json file which causes Webpack to have an undefined 'descriptionFileData'. + await rimraf('package.json'); + await ng('build', '--configuration=development'); +} diff --git a/tests/e2e/tests/build/ts-standard-decorators.ts b/tests/e2e/tests/build/ts-standard-decorators.ts new file mode 100644 index 000000000000..05d056675d3b --- /dev/null +++ b/tests/e2e/tests/build/ts-standard-decorators.ts @@ -0,0 +1,37 @@ +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; +import { updateJsonFile, updateTsConfig } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + // Update project to disable experimental decorators + await updateTsConfig((json) => { + json['compilerOptions']['experimentalDecorators'] = false; + }); + + // Default production build + await ng('build'); + + // Production build with JIT + await updateJsonFile('angular.json', (json) => { + // Remove bundle budgets to avoid a build error due to the expected increased output size + // of a JIT production build. + json.projects['test-project'].architect.build.configurations.production.budgets = []; + }); + + if (!getGlobalVariable('argv')['esbuild']) { + await ng('build', '--no-aot', '--no-build-optimizer'); + } + + // Default development build + await ng('build', '--configuration=development'); + + // Development build with JIT + await ng('build', '--configuration=development', '--no-aot'); + + // Unit tests (JIT only) + await ng('test', '--no-watch'); + + // Ensure application functions in a browser + await executeBrowserTest(); +} diff --git a/tests/e2e/tests/build/wasm-esm.ts b/tests/e2e/tests/build/wasm-esm.ts new file mode 100644 index 000000000000..70633a1021c1 --- /dev/null +++ b/tests/e2e/tests/build/wasm-esm.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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, writeFile } from 'node:fs/promises'; +import assert from 'node:assert/strict'; +import { ng } from '../../utils/process'; +import { prependToFile, replaceInFile } from '../../utils/fs'; +import { updateJsonFile, useSha } from '../../utils/project'; +import { installWorkspacePackages } from '../../utils/packages'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +/** + * 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'); + +export default async function () { + // Add WASM file to project + await writeFile('src/app/multiply.wasm', importWasmBytes); + await writeFile( + 'src/app/multiply.wasm.d.ts', + 'export declare function multiply(a: number, b: number): number; export declare function subtract1(a: number): number;', + ); + + // Add requested WASM import file + await writeFile('src/app/values.js', 'export function getValue() { return 100; }'); + + // Use WASM file in project + await prependToFile( + 'src/app/app.ts', + ` + import { multiply, subtract1 } from './multiply.wasm'; + `, + ); + await replaceInFile('src/app/app.ts', "'test-project'", 'multiply(4, 5) + subtract1(88)'); + + // Remove Zone.js from polyfills and make zoneless + await updateJsonFile('angular.json', (json) => { + // Remove bundle budgets to avoid a build error due to the expected increased output size + // of a JIT production build. + json.projects['test-project'].architect.build.options.polyfills = []; + }); + + await ng('build'); + + // Update E2E test to check for WASM execution + await executeBrowserTest({ expectedTitleText: 'Hello, 32' }); + + // Setup prerendering and build to test Node.js functionality + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await ng('build', '--configuration', 'development', '--prerender'); + const content = await readFile('dist/test-project/browser/index.html', 'utf-8'); + assert.match(content, /Hello, 32/); +} diff --git a/tests/e2e/tests/build/worker.ts b/tests/e2e/tests/build/worker.ts new file mode 100644 index 000000000000..53e60aa34772 --- /dev/null +++ b/tests/e2e/tests/build/worker.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of 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/strict'; +import { readdir } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + + const workerPath = 'src/app/app.worker.ts'; + const snippetPath = 'src/app/app.ts'; + const projectTsConfig = 'tsconfig.json'; + const workerTsConfig = 'tsconfig.worker.json'; + + await ng('generate', 'web-worker', 'app'); + await expectFileToExist(workerPath); + await expectFileToExist(projectTsConfig); + await expectFileToExist(workerTsConfig); + await expectFileToMatch(snippetPath, `new Worker(new URL('./app.worker', import.meta.url)`); + + await ng('build', '--configuration=development'); + if (useWebpackBuilder) { + await expectFileToExist('dist/test-project/browser/src_app_app_worker_ts.js'); + await expectFileToMatch('dist/test-project/browser/main.js', 'src_app_app_worker_ts'); + } else { + const workerOutputFile = await getWorkerOutputFile(false); + await expectFileToExist(`dist/test-project/browser/${workerOutputFile}`); + await expectFileToMatch('dist/test-project/browser/main.js', workerOutputFile); + await expectToFail(() => + expectFileToMatch('dist/test-project/browser/main.js', workerOutputFile + '.map'), + ); + } + + await ng('build', '--output-hashing=none'); + + const workerOutputFile = await getWorkerOutputFile(useWebpackBuilder); + await expectFileToExist(`dist/test-project/browser/${workerOutputFile}`); + if (useWebpackBuilder) { + // Check Webpack builds for the numeric chunk identifier + await expectFileToMatch('dist/test-project/browser/main.js', workerOutputFile.substring(0, 3)); + } else { + await expectFileToMatch('dist/test-project/browser/main.js', workerOutputFile); + } + + // console.warn has to be used because chrome only captures warnings and errors by default + // https://github.com/angular/protractor/issues/2207 + await replaceInFile('src/app/app.ts', 'console.log', 'console.warn'); + + await writeFile( + 'e2e/app.e2e-spec.ts', + ` + import { AppPage } from './app.po'; + import { browser, logging } from 'protractor'; + describe('worker bundle', () => { + it('should log worker messages', async () => { + const page = new AppPage();; + page.navigateTo(); + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + expect(logs.length).toEqual(1); + expect(logs[0].message).toContain('page got message: worker response to hello'); + }); + }); + `, + ); + + await ng('e2e'); +} + +async function getWorkerOutputFile(useWebpackBuilder: boolean): Promise { + const files = await readdir('dist/test-project/browser'); + let fileName; + if (useWebpackBuilder) { + fileName = files.find((f) => /^\d{3}\.js$/.test(f)); + } else { + fileName = files.find((f) => /worker-[\dA-Z]{8}\.js/.test(f)); + } + + assert(fileName, 'Cannot determine worker output file name.'); + + return fileName; +} diff --git a/tests/e2e/tests/commands/add/add-material.ts b/tests/e2e/tests/commands/add/add-material.ts new file mode 100644 index 000000000000..238e5d94dddb --- /dev/null +++ b/tests/e2e/tests/commands/add/add-material.ts @@ -0,0 +1,40 @@ +import { assertIsError } from '../../../utils/utils'; +import { expectFileToMatch, rimraf } from '../../../utils/fs'; +import { getActivePackageManager, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { isPrereleaseCli } from '../../../utils/project'; +import { appendFile } from 'node:fs/promises'; + +export default async function () { + // forcibly remove in case another test doesn't clean itself up + await rimraf('node_modules/@angular/material'); + + const isPrerelease = await isPrereleaseCli(); + const tag = isPrerelease ? '@next' : ''; + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + try { + await ng('add', `@angular/material${tag}`, '--unknown', '--skip-confirmation'); + } catch (error) { + assertIsError(error); + if (!(error as Error).message.includes(`Unknown option: '--unknown'`)) { + throw error; + } + } + + await ng( + 'add', + `@angular/material${tag}`, + '--theme', + 'azure-blue', + '--verbose', + '--skip-confirmation', + ); + await expectFileToMatch('package.json', /@angular\/material/); + + // Clean up existing cdk package + // Not doing so can cause adding material to fail if an incompatible cdk is present + await uninstallPackage('@angular/cdk'); +} diff --git a/tests/e2e/tests/commands/add/add-pwa.ts b/tests/e2e/tests/commands/add/add-pwa.ts new file mode 100644 index 000000000000..c01cc185c84b --- /dev/null +++ b/tests/e2e/tests/commands/add/add-pwa.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToExist, readFile, rimraf } from '../../../utils/fs'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +const snapshots = require('../../../ng-snapshot/package.json'); + +export default async function () { + // forcibly remove in case another test doesn't clean itself up + await rimraf('node_modules/@angular/pwa'); + await ng('add', '@angular/pwa', '--skip-confirmation'); + await expectFileToExist('public/manifest.webmanifest'); + + // Angular PWA doesn't install as a dependency + const { dependencies, devDependencies } = JSON.parse(await readFile('package.json')); + const hasPWADep = Object.keys({ ...dependencies, ...devDependencies }).some( + (d) => d === '@angular/pwa', + ); + assert.ok(!hasPWADep, `Expected 'package.json' not to contain a dependency on '@angular/pwa'.`); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + let needInstall = false; + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries(snapshots.dependencies)) { + if (name in dependencies && dependencies[name] !== version) { + dependencies[name] = version; + needInstall = true; + } + } + }); + + if (needInstall) { + await installWorkspacePackages(); + } + } + + // It should generate a SW configuration file (`ngsw.json`). + const ngswPath = 'dist/test-project/browser/ngsw.json'; + + await ng('build'); + await expectFileToExist(ngswPath); + + // It should correctly generate assetGroups and include at least one URL in each group. + const ngswJson = JSON.parse(await readFile(ngswPath)); + // @ts-ignore + const assetGroups: any[] = ngswJson.assetGroups.map(({ name, urls }) => ({ + name, + urlCount: urls.length, + })); + const emptyAssetGroups = assetGroups.filter(({ urlCount }) => urlCount === 0); + + assert.ok(assetGroups.length > 0, "Expected 'ngsw.json' to contain at least one asset-group."); + assert.ok( + emptyAssetGroups.length === 0, + 'Expected all asset-groups to contain at least one URL, but the following groups are empty: ' + + emptyAssetGroups.map(({ name }) => name).join(', '), + ); +} diff --git a/tests/e2e/tests/commands/add/add-tailwindcss.ts b/tests/e2e/tests/commands/add/add-tailwindcss.ts new file mode 100644 index 000000000000..1444bb6a9a07 --- /dev/null +++ b/tests/e2e/tests/commands/add/add-tailwindcss.ts @@ -0,0 +1,28 @@ +import { expectFileToExist, expectFileToMatch, rimraf } from '../../../utils/fs'; +import { getActivePackageManager, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; + +export default async function () { + // In case a previous test installed tailwindcss, clear it. + // (we don't clear node module directories between tests) + // npm does not appear to fully uninstall sometimes + if (getActivePackageManager() === 'npm') { + await rimraf('node_modules/tailwindcss'); + } + + try { + await ng('add', 'tailwindcss', '--skip-confirmation'); + await expectFileToExist('.postcssrc.json'); + await expectFileToMatch('src/styles.css', /@import "tailwindcss";/); + await expectFileToMatch('package.json', /"tailwindcss":/); + await expectFileToMatch('package.json', /"@tailwindcss\/postcss":/); + await expectFileToMatch('package.json', /"postcss":/); + + // Ensure the project builds + await ng('build', '--configuration=development'); + } finally { + await uninstallPackage('tailwindcss'); + await uninstallPackage('@tailwindcss/postcss'); + await uninstallPackage('postcss'); + } +} diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-version.ts b/tests/e2e/tests/commands/add/add-version.ts similarity index 99% rename from tests/legacy-cli/e2e/tests/commands/add/add-version.ts rename to tests/e2e/tests/commands/add/add-version.ts index bdbf9c96bd64..02d63eb66e0b 100644 --- a/tests/legacy-cli/e2e/tests/commands/add/add-version.ts +++ b/tests/e2e/tests/commands/add/add-version.ts @@ -1,7 +1,6 @@ import { expectFileToExist, expectFileToMatch, rimraf } from '../../../utils/fs'; import { ng } from '../../../utils/process'; - export default async function () { await ng('add', '@angular-devkit-tests/ng-add-simple@^1.0.0', '--skip-confirmation'); await expectFileToMatch('package.json', /\/ng-add-simple.*\^1\.0\.0/); diff --git a/tests/e2e/tests/commands/add/add.ts b/tests/e2e/tests/commands/add/add.ts new file mode 100644 index 000000000000..6d9827013dc4 --- /dev/null +++ b/tests/e2e/tests/commands/add/add.ts @@ -0,0 +1,9 @@ +import { expectFileToExist, expectFileToMatch, rimraf } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + await ng('add', '@angular-devkit-tests/ng-add-simple', '--skip-confirmation'); + await expectFileToMatch('package.json', /@angular-devkit-tests\/ng-add-simple/); + await expectFileToExist('ng-add-test'); + await rimraf('node_modules/@angular-devkit-tests/ng-add-simple'); +} diff --git a/tests/e2e/tests/commands/add/base.ts b/tests/e2e/tests/commands/add/base.ts new file mode 100644 index 000000000000..d31210c6c242 --- /dev/null +++ b/tests/e2e/tests/commands/add/base.ts @@ -0,0 +1,19 @@ +import { assetDir } from '../../../utils/assets'; +import { deleteFile, expectFileToExist, symlinkFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await symlinkFile(assetDir('add-collection-dir'), `./node_modules/add-collection`, 'dir'); + + await ng('add', 'add-collection'); + await expectFileToExist('empty-file'); + + await ng('add', 'add-collection', '--name=blah'); + await expectFileToExist('blah'); + + await expectToFail(() => ng('add', 'add-collection')); // File already exists. + + // Cleanup the package + await deleteFile('node_modules/add-collection'); +} diff --git a/tests/e2e/tests/commands/add/dir.ts b/tests/e2e/tests/commands/add/dir.ts new file mode 100644 index 000000000000..7cb00704cc8e --- /dev/null +++ b/tests/e2e/tests/commands/add/dir.ts @@ -0,0 +1,19 @@ +import { cp } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { assetDir } from '../../../utils/assets'; +import { expectFileToExist } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + const collectionName = 'add-collection-dir'; + const dirCollectionPath = resolve(collectionName); + + // Copy locally as bun doesn't install the dependency correctly if it has symlinks. + await cp(assetDir(collectionName), dirCollectionPath, { + recursive: true, + dereference: true, + }); + + await ng('add', dirCollectionPath, '--name=blah', '--skip-confirmation'); + await expectFileToExist('blah'); +} diff --git a/tests/e2e/tests/commands/add/file.ts b/tests/e2e/tests/commands/add/file.ts new file mode 100644 index 000000000000..5b22b4211ceb --- /dev/null +++ b/tests/e2e/tests/commands/add/file.ts @@ -0,0 +1,13 @@ +import { copyFile } from 'node:fs/promises'; +import { assetDir } from '../../../utils/assets'; +import { expectFileToExist } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + // Avoids ERR_PNPM_ENAMETOOLONG errors. + const tarball = './add-collection.tgz'; + await copyFile(assetDir(tarball), tarball); + + await ng('add', tarball, '--name=blah', '--skip-confirmation'); + await expectFileToExist('blah'); +} diff --git a/tests/e2e/tests/commands/add/npm-config.ts b/tests/e2e/tests/commands/add/npm-config.ts new file mode 100644 index 000000000000..6f7599e8c00c --- /dev/null +++ b/tests/e2e/tests/commands/add/npm-config.ts @@ -0,0 +1,8 @@ +import { writeFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + // Works with before option + await writeFile('.npmrc', `before=${new Date().toISOString()}`); + await ng('add', '@angular/pwa', '--skip-confirmation'); +} diff --git a/tests/e2e/tests/commands/add/peer.ts b/tests/e2e/tests/commands/add/peer.ts new file mode 100644 index 000000000000..143542e4533c --- /dev/null +++ b/tests/e2e/tests/commands/add/peer.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import { cp } from 'node:fs/promises'; +import { assetDir } from '../../../utils/assets'; +import { ng } from '../../../utils/process'; + +export default async function (): Promise { + const warning = /Adding the package may not succeed/; + + const stdout1 = await runNgAdd('add-collection-peer-bad'); + assert.match( + stdout1, + warning, + `Peer warning should be shown for add-collection-peer-bad but was not.`, + ); + + const stdout2 = await runNgAdd('add-collection-dir'); + assert.doesNotMatch( + stdout2, + warning, + `Peer warning should NOT be shown for add-collection-dir but was.`, + ); + + const stdout3 = await runNgAdd('add-collection-peer-good'); + assert.doesNotMatch( + stdout3, + warning, + `Peer warning should NOT be shown for add-collection-peer-good but was.`, + ); +} + +async function runNgAdd(collectionName: string): Promise { + const collectionPath = resolve(collectionName); + + // Copy locally as bun doesn't install the dependency correctly if it has symlinks. + await cp(assetDir(collectionName), collectionPath, { + recursive: true, + dereference: true, + }); + + const { stdout } = await ng('add', collectionPath, '--skip-confirmation'); + + return stdout; +} diff --git a/tests/e2e/tests/commands/add/registry-option.ts b/tests/e2e/tests/commands/add/registry-option.ts new file mode 100644 index 000000000000..dc336cdd5b30 --- /dev/null +++ b/tests/e2e/tests/commands/add/registry-option.ts @@ -0,0 +1,16 @@ +import { getGlobalVariable } from '../../../utils/env'; +import { expectFileToExist } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + const testRegistry = getGlobalVariable('package-registry'); + + // Set an invalid registry + process.env['NPM_CONFIG_REGISTRY'] = 'http://127.0.0.1:9999'; + + await expectToFail(() => ng('add', '@angular/pwa', '--skip-confirmation')); + + await ng('add', `--registry=${testRegistry}`, '@angular/pwa', '--skip-confirmation'); + await expectFileToExist('public/manifest.webmanifest'); +} diff --git a/tests/e2e/tests/commands/add/secure-registry.ts b/tests/e2e/tests/commands/add/secure-registry.ts new file mode 100644 index 000000000000..4a640607f8be --- /dev/null +++ b/tests/e2e/tests/commands/add/secure-registry.ts @@ -0,0 +1,50 @@ +import { expectFileNotToExist, expectFileToExist, rimraf } from '../../../utils/fs'; +import { getActivePackageManager, installWorkspacePackages } from '../../../utils/packages'; +import { git, ng } from '../../../utils/process'; +import { createNpmConfigForAuthentication } from '../../../utils/registry'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + const originalNpmConfigRegistry = process.env['NPM_CONFIG_REGISTRY']; + try { + // The environment variable has priority over the .npmrc + delete process.env['NPM_CONFIG_REGISTRY']; + const packageManager = getActivePackageManager(); + const supportsUnscopedAuth = packageManager === 'yarn'; + const command = ['add', '@angular/pwa', '--skip-confirmation']; + + // Works with unscoped registry authentication details + if (supportsUnscopedAuth) { + // Some package managers such as Bun and NPM do not support unscoped auth. + await createNpmConfigForAuthentication(false); + + await expectFileNotToExist('public/manifest.webmanifest'); + + await ng(...command); + await expectFileToExist('public/manifest.webmanifest'); + await git('clean', '-dxf'); + } + + // Works with scoped registry authentication details + await expectFileNotToExist('public/manifest.webmanifest'); + + await createNpmConfigForAuthentication(true); + await ng(...command); + await expectFileToExist('public/manifest.webmanifest'); + await git('clean', '-dxf'); + + // Invalid authentication token + if (supportsUnscopedAuth) { + // Some package managers such as Bun and NPM do not support unscoped auth. + await createNpmConfigForAuthentication(false, true); + await expectToFail(() => ng(...command)); + } + + await createNpmConfigForAuthentication(true, true); + await expectToFail(() => ng(...command)); + } finally { + process.env['NPM_CONFIG_REGISTRY'] = originalNpmConfigRegistry; + await git('clean', '-dxf'); + await installWorkspacePackages(); + } +} diff --git a/tests/e2e/tests/commands/add/version-specifier.ts b/tests/e2e/tests/commands/add/version-specifier.ts new file mode 100644 index 000000000000..f88d60a51ec9 --- /dev/null +++ b/tests/e2e/tests/commands/add/version-specifier.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { appendFile } from 'node:fs/promises'; +import { expectFileToMatch } from '../../../utils/fs'; +import { getActivePackageManager, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { isPrereleaseCli } from '../../../utils/project'; + +export default async function () { + // forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/localize'); + + // If using npm, enable the legacy-peer-deps option to allow testing the output behavior of the + // `ng add` command itself and not the behavior of npm which may otherwise fail depending + // on the npm version in use and the version specifier supplied in each test. + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true\n'); + } + + const tag = isPrereleaseCli() ? '@next' : ''; + + await ng('add', `@angular/localize${tag}`, '--skip-confirmation'); + await expectFileToMatch('package.json', /@angular\/localize/); + + const output1 = await ng('add', '@angular/localize', '--skip-confirmation'); + assert.match( + output1.stdout, + /Skipping installation: Package already installed/, + 'Installation was not skipped', + ); + + const output2 = await ng('add', '@angular/localize@latest', '--skip-confirmation'); + assert.doesNotMatch( + output2.stdout, + /Skipping installation: Package already installed/, + 'Installation should not have been skipped', + ); + + const output3 = await ng('add', '@angular/localize@19.1.0', '--skip-confirmation'); + assert.doesNotMatch( + output3.stdout, + /Skipping installation: Package already installed/, + 'Installation should not have been skipped', + ); + + const output4 = await ng('add', '@angular/localize@19', '--skip-confirmation'); + assert.match( + output4.stdout, + /Skipping installation: Package already installed/, + 'Installation was not skipped', + ); + + await uninstallPackage('@angular/localize'); +} diff --git a/tests/e2e/tests/commands/add/yarn-env-vars.ts b/tests/e2e/tests/commands/add/yarn-env-vars.ts new file mode 100644 index 000000000000..103a99c04056 --- /dev/null +++ b/tests/e2e/tests/commands/add/yarn-env-vars.ts @@ -0,0 +1,29 @@ +import { expectFileNotToExist, expectFileToExist } from '../../../utils/fs'; +import { getActivePackageManager } from '../../../utils/packages'; +import { git, ng } from '../../../utils/process'; +import { + createNpmConfigForAuthentication, + setNpmEnvVarsForAuthentication, +} from '../../../utils/registry'; + +export default async function () { + // Yarn specific test that tests YARN_ env variables. + // https://classic.yarnpkg.com/en/docs/envvars/ + if (getActivePackageManager() !== 'yarn') { + return; + } + const command = ['add', '@angular/pwa', '--skip-confirmation']; + + // Environment variables only + await expectFileNotToExist('public/manifest.webmanifest'); + setNpmEnvVarsForAuthentication(false, true); + await ng(...command); + await expectFileToExist('public/manifest.webmanifest'); + await git('clean', '-dxf'); + + // Mix of config file and env vars works + await expectFileNotToExist('public/manifest.webmanifest'); + await createNpmConfigForAuthentication(false, true); + await ng(...command); + await expectFileToExist('public/manifest.webmanifest'); +} diff --git a/tests/e2e/tests/commands/additional-properties.ts b/tests/e2e/tests/commands/additional-properties.ts new file mode 100644 index 000000000000..a53006853fca --- /dev/null +++ b/tests/e2e/tests/commands/additional-properties.ts @@ -0,0 +1,31 @@ +import { createDir, rimraf, writeMultipleFiles } from '../../utils/fs'; +import { execAndWaitForOutputToMatch } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + await createDir('example-builder'); + await writeMultipleFiles({ + 'example-builder/package.json': '{ "builders": "./builders.json" }', + 'example-builder/schema.json': + '{ "$schema": "http://json-schema.org/draft-07/schema", "type": "object", "additionalProperties": true }', + 'example-builder/builders.json': + '{ "$schema": "@angular-devkit/architect/src/builders-schema.json", "builders": { "example": { "implementation": "./example", "schema": "./schema.json" } } }', + 'example-builder/example.js': + 'module.exports.default = require("@angular-devkit/architect").createBuilder((options) => { console.log(options); return { success: true }; });', + }); + + await updateJsonFile('angular.json', (json) => { + const appArchitect = json.projects['test-project'].architect; + appArchitect.example = { + builder: './example-builder:example', + }; + }); + + await execAndWaitForOutputToMatch( + 'ng', + ['run', 'test-project:example', '--additional', 'property'], + /Unknown argument: additional/, + ); + + await rimraf('example-builder'); +} diff --git a/tests/e2e/tests/commands/analytics/analytics-enable-disable.ts b/tests/e2e/tests/commands/analytics/analytics-enable-disable.ts new file mode 100644 index 000000000000..94bd8f95edb2 --- /dev/null +++ b/tests/e2e/tests/commands/analytics/analytics-enable-disable.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert'; +import { readFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + await ng('analytics', 'enable'); + assert.ok(JSON.parse(await readFile('angular.json')).cli.analytics); + + await ng('analytics', 'disable'); + assert.strictEqual(JSON.parse(await readFile('angular.json')).cli.analytics, false); +} diff --git a/tests/e2e/tests/commands/analytics/analytics-info.ts b/tests/e2e/tests/commands/analytics/analytics-info.ts new file mode 100644 index 000000000000..d92dfd2ffde6 --- /dev/null +++ b/tests/e2e/tests/commands/analytics/analytics-info.ts @@ -0,0 +1,30 @@ +import { execAndWaitForOutputToMatch } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +export default async function () { + // Should be disabled by default. + await configureTest(undefined /** analytics */); + await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: disabled/, { + ...process.env, + NG_FORCE_TTY: '0', // Disable prompts + }); + + await configureTest('1dba0835-38a3-4957-bf34-9974e2df0df3' /** analytics */); + await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: enabled/, { + ...process.env, + NG_FORCE_TTY: '0', // Disable prompts + }); + + await configureTest(false /** analytics */); + await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: disabled/, { + ...process.env, + NG_FORCE_TTY: '0', // Disable prompts + }); +} + +async function configureTest(analytics: false | string | undefined): Promise { + await updateJsonFile('angular.json', (config) => { + config.cli ??= {}; + config.cli.analytics = analytics; + }); +} diff --git a/tests/e2e/tests/commands/analytics/ask-analytics-command.ts b/tests/e2e/tests/commands/analytics/ask-analytics-command.ts new file mode 100644 index 000000000000..ae20bb21f2fa --- /dev/null +++ b/tests/e2e/tests/commands/analytics/ask-analytics-command.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { execWithEnv } from '../../../utils/process'; +import { mockHome } from '../../../utils/utils'; + +const ANALYTICS_PROMPT = /Would you like to share pseudonymous usage data/; + +export default async function () { + // CLI should prompt for analytics permissions. + await mockHome(async () => { + const { stdout } = await execWithEnv( + 'ng', + ['config'], + { + ...process.env, + NG_FORCE_TTY: '1', + NG_FORCE_AUTOCOMPLETE: 'false', + }, + 'n\n' /* stdin */, + ); + + assert.match(stdout, ANALYTICS_PROMPT, 'CLI did not prompt for analytics permission.'); + }); + + // CLI should skip analytics prompt with `NG_CLI_ANALYTICS=false`. + await mockHome(async () => { + const { stdout } = await execWithEnv('ng', ['config'], { + ...process.env, + NG_FORCE_TTY: '1', + NG_CLI_ANALYTICS: 'false', + NG_FORCE_AUTOCOMPLETE: 'false', + }); + + assert.doesNotMatch( + stdout, + ANALYTICS_PROMPT, + 'CLI prompted for analytics permission when it should be forced off.', + ); + }); + + // CLI should skip analytics prompt during `ng update`. + await mockHome(async () => { + const { stdout } = await execWithEnv('ng', ['update', '--help'], { + ...process.env, + NG_FORCE_TTY: '1', + NG_FORCE_AUTOCOMPLETE: 'false', + }); + + assert.doesNotMatch( + stdout, + ANALYTICS_PROMPT, + 'CLI prompted for analytics permission during an update where it should not have.', + ); + }); +} diff --git a/tests/e2e/tests/commands/builder-not-found.ts b/tests/e2e/tests/commands/builder-not-found.ts new file mode 100644 index 000000000000..ec0002ef65d3 --- /dev/null +++ b/tests/e2e/tests/commands/builder-not-found.ts @@ -0,0 +1,45 @@ +import { moveFile } from '../../utils/fs'; +import { getActivePackageManager, installPackage, uninstallPackage } from '../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + try { + await uninstallPackage('@angular-devkit/build-angular'); + + await expectToFail(() => ng('build')); + await execAndWaitForOutputToMatch( + 'ng', + ['build'], + /Could not find the '@angular-devkit\/build-angular:browser' builder's node package\./, + ); + await expectToFail(() => + execAndWaitForOutputToMatch( + 'ng', + ['build'], + new RegExp( + `Node packages may not be installed\\. Try installing with '${getActivePackageManager()} install'\\.`, + ), + ), + ); + + await moveFile('node_modules', 'temp_node_modules'); + + await expectToFail(() => ng('build')); + await execAndWaitForOutputToMatch( + 'ng', + ['build'], + /Could not find the '@angular-devkit\/build-angular:browser' builder's node package\./, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['build'], + new RegExp( + `Node packages may not be installed\\. Try installing with '${getActivePackageManager()} install'\\.`, + ), + ); + } finally { + await moveFile('temp_node_modules', 'node_modules'); + await installPackage('@angular-devkit/build-angular'); + } +} diff --git a/tests/e2e/tests/commands/builder-project-by-cwd.ts b/tests/e2e/tests/commands/builder-project-by-cwd.ts new file mode 100644 index 000000000000..6033f4542391 --- /dev/null +++ b/tests/e2e/tests/commands/builder-project-by-cwd.ts @@ -0,0 +1,21 @@ +import { join } from 'node:path'; +import { expectFileToExist } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + await ng('generate', 'app', 'second-app', '--skip-install'); + await ng('generate', 'app', 'third-app', '--skip-install'); + const startCwd = process.cwd(); + + try { + // When no project is provided it should favor the project that is located in the current working directory. + process.chdir(join(startCwd, 'projects/second-app')); + await ng('build', '--configuration=development'); + + process.chdir(startCwd); + await expectFileToExist('dist/second-app'); + } finally { + // restore path + process.chdir(startCwd); + } +} diff --git a/tests/e2e/tests/commands/cache/cache-clean.ts b/tests/e2e/tests/commands/cache/cache-clean.ts new file mode 100644 index 000000000000..004c5a069dfc --- /dev/null +++ b/tests/e2e/tests/commands/cache/cache-clean.ts @@ -0,0 +1,11 @@ +import { createDir, expectFileNotToExist, expectFileToExist } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + const cachePath = '.angular/cache'; + await createDir(cachePath); + await expectFileToExist(cachePath); + + await ng('cache', 'clean'); + await expectFileNotToExist(cachePath); +} diff --git a/tests/e2e/tests/commands/cache/cache-enable-disable.ts b/tests/e2e/tests/commands/cache/cache-enable-disable.ts new file mode 100644 index 000000000000..1cfbf705787e --- /dev/null +++ b/tests/e2e/tests/commands/cache/cache-enable-disable.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert/strict'; +import { readFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; + +export default async function () { + await ng('cache', 'enable'); + assert.strictEqual( + JSON.parse(await readFile('angular.json')).cli.cache.enabled, + true, + `Expected 'cli.cache.enable' to be true.`, + ); + + await ng('cache', 'disable'); + assert.strictEqual( + JSON.parse(await readFile('angular.json')).cli.cache.enabled, + false, + `Expected 'cli.cache.enable' to be false.`, + ); +} diff --git a/tests/e2e/tests/commands/cache/cache-info.ts b/tests/e2e/tests/commands/cache/cache-info.ts new file mode 100644 index 000000000000..bad2df6c3bdc --- /dev/null +++ b/tests/e2e/tests/commands/cache/cache-info.ts @@ -0,0 +1,78 @@ +import { execAndWaitForOutputToMatch } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; + +const ENV_NO_COLOR = { + 'NO_COLOR': '1', +}; + +export default async function () { + const originalCIValue = process.env['CI']; + + try { + // Should be enabled by default for local builds. + await configureTest('0' /** envCI */); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Enabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Should be disabled by default for CI builds. + await configureTest('1' /** envCI */, { enabled: true }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Disabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Should be enabled by when environment is local and env is not CI. + await configureTest('0' /** envCI */, { environment: 'local' }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Enabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Should be disabled by when environment is local and env is CI. + await configureTest('1' /** envCI */, { environment: 'local' }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Disabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Effective status should be enabled when 'environment' is set to 'all' or 'ci'. + await configureTest('1' /** envCI */, { environment: 'all' }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Enabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Effective status should be enabled when 'environment' is set to 'ci' and run is in ci + await configureTest('1' /** envCI */, { environment: 'ci' }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Enabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + + // Effective status should be disabled when 'enabled' is set to false + await configureTest('1' /** envCI */, { environment: 'all', enabled: false }); + await execAndWaitForOutputToMatch('ng', ['cache', 'info'], /Effective Status\s*: Disabled/, { + ...process.env, + ...ENV_NO_COLOR, + }); + } finally { + process.env['CI'] = originalCIValue; + } +} + +async function configureTest( + envCI: '1' | '0', + cacheOptions?: { + environment?: 'ci' | 'local' | 'all'; + enabled?: boolean; + }, +): Promise { + process.env['CI'] = envCI; + + await updateJsonFile('angular.json', (config) => { + config.cli ??= {}; + config.cli.cache = cacheOptions; + }); +} diff --git a/tests/e2e/tests/commands/completion/completion-prompt.ts b/tests/e2e/tests/commands/completion/completion-prompt.ts new file mode 100644 index 000000000000..424b373e47a6 --- /dev/null +++ b/tests/e2e/tests/commands/completion/completion-prompt.ts @@ -0,0 +1,470 @@ +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +import { env } from 'node:process'; +import { getGlobalVariable } from '../../../utils/env'; +import { mockHome } from '../../../utils/utils'; +import assert from 'node:assert/strict'; + +import { + execAndCaptureError, + execAndWaitForOutputToMatch, + execWithEnv, + silentNpm, +} from '../../../utils/process'; + +const AUTOCOMPLETION_PROMPT = /Would you like to enable autocompletion\?/; +const DEFAULT_ENV = Object.freeze({ + ...env, + // Shell should be mocked for each test that cares about it. + SHELL: '/bin/bash', + // Even if the actual test process is run on CI, we're testing user flows which aren't on CI. + CI: undefined, + // Tests run on CI technically don't have a TTY, but the autocompletion prompt requires it, so we + // force a TTY by default. + NG_FORCE_TTY: '1', + // Analytics wants to prompt for a first command as well, but we don't care about that here. + NG_CLI_ANALYTICS: 'false', +}); + +const testRegistry = getGlobalVariable('package-registry'); + +export default async function () { + // Windows Cmd and Powershell do not support autocompletion. Run a different set of tests to + // confirm autocompletion skips the prompt appropriately. + if (process.platform === 'win32') { + await windowsTests(); + return; + } + + // Sets up autocompletion after user accepts a prompt from any command. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, `# Other content...`); + + const { stdout } = await execWithEnv( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'y\n' /* stdin: accept prompt */, + ); + + assert.match( + stdout, + AUTOCOMPLETION_PROMPT, + 'CLI execution did not prompt for autocompletion setup when it should have.', + ); + + const bashrcContents = await fs.readFile(bashrc, 'utf-8'); + assert.match( + bashrcContents, + /source <\(ng completion script\)/, + 'Autocompletion was *not* added to `~/.bashrc` after accepting the setup prompt.', + ); + + assert.match( + stdout, + /Appended `source <\(ng completion script\)`/, + 'CLI did not print that it successfully set up autocompletion.', + ); + }); + + // Does nothing if the user rejects the autocompletion prompt. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, `# Other content...`); + + const { stdout } = await execWithEnv( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'n\n' /* stdin: reject prompt */, + ); + + assert.match( + stdout, + AUTOCOMPLETION_PROMPT, + 'CLI execution did not prompt for autocompletion setup when it should have.', + ); + + const bashrcContents = await fs.readFile(bashrc, 'utf-8'); + assert.doesNotMatch( + bashrcContents, + /ng completion/, + 'Autocompletion was incorrectly added to `~/.bashrc` after refusing the setup prompt.', + ); + + assert.doesNotMatch( + stdout, + /Appended `source <\(ng completion script\)`/, + "CLI printed that it successfully set up autocompletion when it actually didn't.", + ); + + assert.match( + stdout, + /Ok, you won't be prompted again\./, + 'CLI did not inform the user they will not be prompted again.', + ); + }); + + // Does *not* prompt if the user already accepted (even if they delete the completion config). + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, '# Other commands...'); + + const { stdout: stdout1 } = await execWithEnv( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'y\n' /* stdin: accept prompt */, + ); + + assert.match( + stdout1, + AUTOCOMPLETION_PROMPT, + 'First execution did not prompt for autocompletion setup.', + ); + + const bashrcContents1 = await fs.readFile(bashrc, 'utf-8'); + assert.match( + bashrcContents1, + /source <\(ng completion script\)/, + '`~/.bashrc` file was not updated after the user accepted the autocompletion' + + ` prompt. Contents:\n${bashrcContents1}`, + ); + + // User modifies their configuration and removes `ng completion`. + await fs.writeFile(bashrc, '# Some new commands...'); + + const { stdout: stdout2 } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }); + + assert.doesNotMatch( + stdout2, + AUTOCOMPLETION_PROMPT, + 'Subsequent execution after rejecting autocompletion setup prompted again' + + ' when it should not have.', + ); + + const bashrcContents2 = await fs.readFile(bashrc, 'utf-8'); + assert.strictEqual( + bashrcContents2, + '# Some new commands...', + '`~/.bashrc` file was incorrectly modified when using a modified `~/.bashrc`' + + ` after previously accepting the autocompletion prompt. Contents:\n${bashrcContents2}`, + ); + }); + + // Does *not* prompt if the user already rejected. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, '# Other commands...'); + + const { stdout: stdout1 } = await execWithEnv( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'n\n' /* stdin: reject prompt */, + ); + + assert.match( + stdout1, + AUTOCOMPLETION_PROMPT, + 'First execution did not prompt for autocompletion setup.', + ); + + const { stdout: stdout2 } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }); + + assert.doesNotMatch( + stdout2, + AUTOCOMPLETION_PROMPT, + 'Subsequent execution after rejecting autocompletion setup prompted again' + + ' when it should not have.', + ); + + const bashrcContents = await fs.readFile(bashrc, 'utf-8'); + assert.strictEqual( + bashrcContents, + '# Other commands...', + '`~/.bashrc` file was incorrectly modified when the user never accepted the' + + ` autocompletion prompt. Contents:\n${bashrcContents}`, + ); + }); + + // Prompts user again on subsequent execution after accepting prompt but failing to setup. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, '# Other commands...'); + + // Make `~/.bashrc` readonly. This is enough for the CLI to verify that the file exists and + // `ng completion` is not in it, but will fail when actually trying to modify the file. + await fs.chmod(bashrc, 0o444); + + const err = await execAndCaptureError( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'y\n' /* stdin: accept prompt */, + ); + + assert.match( + err.message, + /Failed to append autocompletion setup/, + `Failed first execution did not print the expected error message. Actual:\n${err.message}`, + ); + + // User corrects file permissions between executions. + await fs.chmod(bashrc, 0o777); + + const { stdout: stdout2 } = await execWithEnv( + 'ng', + ['config'], + { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }, + 'y\n' /* stdin: accept prompt */, + ); + + assert.match( + stdout2, + AUTOCOMPLETION_PROMPT, + 'Subsequent execution after failed autocompletion setup did not prompt again when it should' + + ' have.', + ); + + const bashrcContents = await fs.readFile(bashrc, 'utf-8'); + assert.match( + bashrcContents, + /ng completion script/, + '`~/.bashrc` file does not include `ng completion` after the user never accepted the' + + ` autocompletion prompt a second time. Contents:\n${bashrcContents}`, + ); + }); + + // Does *not* prompt for `ng update` commands. + await mockHome(async (home) => { + // Use `ng update --help` so it's actually a no-op and we don't need to setup a project. + const { stdout } = await execWithEnv('ng', ['update', '--help'], { + ...DEFAULT_ENV, + HOME: home, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + '`ng update` command incorrectly prompted for autocompletion setup.', + ); + }); + + // Does *not* prompt for `ng completion` commands. + await mockHome(async (home) => { + const { stdout } = await execWithEnv('ng', ['completion'], { + ...DEFAULT_ENV, + HOME: home, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + '`ng completion` command incorrectly prompted for autocompletion setup.', + ); + }); + + // Does *not* prompt user for CI executions. + { + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + CI: 'true', + NG_FORCE_TTY: undefined, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'CI execution prompted for autocompletion setup but should not have.', + ); + } + + // Does *not* prompt user for non-TTY executions. + { + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + NG_FORCE_TTY: 'false', + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Non-TTY execution prompted for autocompletion setup but should not have.', + ); + } + + // Does *not* prompt user for executions without a `$HOME`. + { + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + HOME: undefined, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Execution without a `$HOME` value prompted for autocompletion setup but' + + ' should not have.', + ); + } + + // Does *not* prompt user for executions without a `$SHELL`. + { + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + SHELL: undefined, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Execution without a `$SHELL` value prompted for autocompletion setup but' + + ' should not have.', + ); + } + + // Does *not* prompt user for executions from unknown shells. + { + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + SHELL: '/usr/bin/unknown', + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Execution with an unknown `$SHELL` value prompted for autocompletion setup' + + ' but should not have.', + ); + } + + // Does *not* prompt user when an RC file already uses `ng completion`. + await mockHome(async (home) => { + await fs.writeFile( + path.join(home, '.bashrc'), + ` +# Some stuff... + +source <(ng completion script) + +# Some other stuff... + `.trim(), + ); + + const { stdout } = await execWithEnv('ng', ['config'], { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + "Execution with an existing `ng completion` line in the user's RC file" + + ' prompted for autocompletion setup but should not have.', + ); + }); + + // Prompts when a global CLI install is present on the system. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, `# Other content...`); + + await execAndWaitForOutputToMatch('ng', ['config'], AUTOCOMPLETION_PROMPT, { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + }); + }); + + // Does *not* prompt when a global CLI install is missing from the system. + await mockHome(async (home) => { + try { + // Temporarily uninstall the global CLI binary from the system. + await silentNpm(['uninstall', '--global', '@angular/cli', `--registry=${testRegistry}`]); + + // Setup a fake project directory with a local install of the CLI. + const projectDir = path.join(home, 'project'); + await fs.mkdir(projectDir); + await silentNpm(['init', '-y', `--registry=${testRegistry}`], { cwd: projectDir }); + await silentNpm(['install', '@angular/cli', `--registry=${testRegistry}`], { + cwd: projectDir, + }); + + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, `# Other content...`); + + const localCliDir = path.join(projectDir, 'node_modules', '.bin'); + const localCliBinary = path.join(localCliDir, 'ng'); + const pathDirs = process.env['PATH']!.split(':'); + const pathEnvVar = [...pathDirs, localCliDir].join(':'); + const { stdout } = await execWithEnv(localCliBinary, ['config'], { + ...DEFAULT_ENV, + SHELL: '/bin/bash', + HOME: home, + PATH: pathEnvVar, + }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Execution without a global CLI install prompted for autocompletion setup but should' + + ' not have.', + ); + } finally { + // Reinstall global CLI for remainder of the tests. + await silentNpm(['install', '--global', '@angular/cli', `--registry=${testRegistry}`]); + } + }); +} + +async function windowsTests(): Promise { + // Should *not* prompt on Windows, autocompletion isn't supported. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, `# Other content...`); + + const { stdout } = await execWithEnv('ng', ['config'], { ...env }); + + assert.doesNotMatch( + stdout, + AUTOCOMPLETION_PROMPT, + 'Execution prompted to set up autocompletion on Windows despite not actually being' + + ' supported.', + ); + }); +} diff --git a/tests/e2e/tests/commands/completion/completion-script.ts b/tests/e2e/tests/commands/completion/completion-script.ts new file mode 100644 index 000000000000..421763478950 --- /dev/null +++ b/tests/e2e/tests/commands/completion/completion-script.ts @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { exec, execAndWaitForOutputToMatch } from '../../../utils/process'; + +export default async function () { + // ng build + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'b', ''], + /test-project/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'build', ''], + /test-project/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'build', '--a'], + /--aot/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'build', '--configuration'], + /production/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'b', '--configuration'], + /production/, + ); + + // ng run + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'run', ''], + /test-project\\:build\\:development/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'run', ''], + /test-project\\:build/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'run', ''], + /test-project\\:test/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'run', 'test-project:build'], + /test-project\\:build\\:development/, + ); + await execAndWaitForOutputToMatch( + 'ng', + ['--get-yargs-completions', 'ng', 'run', 'test-project:'], + /test-project\\:test/, + ); + + const { stdout: noServeStdout } = await exec( + 'ng', + '--get-yargs-completions', + 'ng', + 'run', + 'test-project:build', + ); + assert.doesNotMatch( + noServeStdout, + /:serve/, + `':serve' should not have been listed as a completion option.\nSTDOUT:\n${noServeStdout}`, + ); +} diff --git a/tests/e2e/tests/commands/completion/completion.ts b/tests/e2e/tests/commands/completion/completion.ts new file mode 100644 index 000000000000..939bdb8c306b --- /dev/null +++ b/tests/e2e/tests/commands/completion/completion.ts @@ -0,0 +1,383 @@ +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +import { getGlobalVariable } from '../../../utils/env'; +import { mockHome } from '../../../utils/utils'; +import { + execAndCaptureError, + execAndWaitForOutputToMatch, + execWithEnv, + silentNpm, +} from '../../../utils/process'; + +const testRegistry = getGlobalVariable('package-registry'); + +export default async function () { + // Windows Cmd and Powershell do not support autocompletion. Run a different set of tests to + // confirm autocompletion fails gracefully. + if (process.platform === 'win32') { + await windowsTests(); + + return; + } + + // Generates new `.bashrc` file. + await mockHome(async (home) => { + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/bin/bash', + }, + ); + + const rcContents = await fs.readFile(path.join(home, '.bashrc'), 'utf-8'); + const expected = ` +# Load Angular CLI autocompletion. +source <(ng completion script) + `.trim(); + if (!rcContents.includes(expected)) { + throw new Error(`~/.bashrc does not contain autocompletion script. Contents:\n${rcContents}`); + } + }); + + // Generates new `.zshrc` file. + await mockHome(async (home) => { + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }, + ); + + const rcContents = await fs.readFile(path.join(home, '.zshrc'), 'utf-8'); + const expected = ` +# Load Angular CLI autocompletion. +source <(ng completion script) + `.trim(); + if (!rcContents.includes(expected)) { + throw new Error(`~/.zshrc does not contain autocompletion script. Contents:\n${rcContents}`); + } + }); + + // Appends to existing `.bashrc` file. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/bin/bash', + }, + ); + + const rcContents = await fs.readFile(bashrc, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.bashrc does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Appends to existing `.bash_profile` file. + await mockHome(async (home) => { + const bashProfile = path.join(home, '.bash_profile'); + await fs.writeFile(bashProfile, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/bin/bash', + }, + ); + + const rcContents = await fs.readFile(bashProfile, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.bash_profile does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Appends to existing `.profile` file (using Bash). + await mockHome(async (home) => { + const profile = path.join(home, '.profile'); + await fs.writeFile(profile, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/bin/bash', + }, + ); + + const rcContents = await fs.readFile(profile, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.profile does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Bash shell prefers `.bashrc`. + await mockHome(async (home) => { + const bashrc = path.join(home, '.bashrc'); + await fs.writeFile(bashrc, '# `.bashrc` commands...'); + const bashProfile = path.join(home, '.bash_profile'); + await fs.writeFile(bashProfile, '# `.bash_profile` commands...'); + const profile = path.join(home, '.profile'); + await fs.writeFile(profile, '# `.profile` commands...'); + + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/bin/bash', + }, + ); + + const bashrcContents = await fs.readFile(bashrc, 'utf-8'); + const bashrcExpected = `# \`.bashrc\` commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (bashrcContents !== bashrcExpected) { + throw new Error(`~/.bashrc does not match expectation. Contents:\n${bashrcContents}`); + } + const bashProfileContents = await fs.readFile(bashProfile, 'utf-8'); + if (bashProfileContents !== '# `.bash_profile` commands...') { + throw new Error( + `~/.bash_profile does not match expectation. Contents:\n${bashProfileContents}`, + ); + } + const profileContents = await fs.readFile(profile, 'utf-8'); + if (profileContents !== '# `.profile` commands...') { + throw new Error(`~/.profile does not match expectation. Contents:\n${profileContents}`); + } + }); + + // Appends to existing `.zshrc` file. + await mockHome(async (home) => { + const zshrc = path.join(home, '.zshrc'); + await fs.writeFile(zshrc, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }, + ); + + const rcContents = await fs.readFile(zshrc, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.zshrc does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Appends to existing `.zsh_profile` file. + await mockHome(async (home) => { + const zshProfile = path.join(home, '.zsh_profile'); + await fs.writeFile(zshProfile, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }, + ); + + const rcContents = await fs.readFile(zshProfile, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.zsh_profile does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Appends to existing `.profile` file (using Zsh). + await mockHome(async (home) => { + const profile = path.join(home, '.profile'); + await fs.writeFile(profile, '# Other commands...'); + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }, + ); + + const rcContents = await fs.readFile(profile, 'utf-8'); + const expected = `# Other commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (rcContents !== expected) { + throw new Error(`~/.profile does not match expectation. Contents:\n${rcContents}`); + } + }); + + // Zsh prefers `.zshrc`. + await mockHome(async (home) => { + const zshrc = path.join(home, '.zshrc'); + await fs.writeFile(zshrc, '# `.zshrc` commands...'); + const zshProfile = path.join(home, '.zsh_profile'); + await fs.writeFile(zshProfile, '# `.zsh_profile` commands...'); + const profile = path.join(home, '.profile'); + await fs.writeFile(profile, '# `.profile` commands...'); + + await execAndWaitForOutputToMatch( + 'ng', + ['completion'], + /Appended `source <\(ng completion script\)`/, + { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }, + ); + + const zshrcContents = await fs.readFile(zshrc, 'utf-8'); + const zshrcExpected = `# \`.zshrc\` commands... + +# Load Angular CLI autocompletion. +source <(ng completion script) +`; + if (zshrcContents !== zshrcExpected) { + throw new Error(`~/.zshrc does not match expectation. Contents:\n${zshrcContents}`); + } + + const zshProfileContents = await fs.readFile(zshProfile, 'utf-8'); + if (zshProfileContents !== '# `.zsh_profile` commands...') { + throw new Error( + `~/.zsh_profile does not match expectation. Contents:\n${zshProfileContents}`, + ); + } + const profileContents = await fs.readFile(profile, 'utf-8'); + if (profileContents !== '# `.profile` commands...') { + throw new Error(`~/.profile does not match expectation. Contents:\n${profileContents}`); + } + }); + + // Fails for no `$HOME` directory. + { + const err = await execAndCaptureError('ng', ['completion'], { + ...process.env, + SHELL: '/bin/bash', + HOME: undefined, + }); + if (!err.message.includes('`$HOME` environment variable not set.')) { + throw new Error(`Expected unset \`$HOME\` error message, but got:\n\n${err.message}`); + } + } + + // Fails for no `$SHELL`. + await mockHome(async (home) => { + const err = await execAndCaptureError('ng', ['completion'], { + ...process.env, + SHELL: undefined, + }); + if (!err.message.includes('`$SHELL` environment variable not set.')) { + throw new Error(`Expected unset \`$SHELL\` error message, but got:\n\n${err.message}`); + } + }); + + // Fails for unknown `$SHELL`. + await mockHome(async (home) => { + const err = await execAndCaptureError('ng', ['completion'], { + ...process.env, + SHELL: '/usr/bin/unknown', + }); + if (!err.message.includes('Unknown `$SHELL` environment variable')) { + throw new Error(`Expected unknown \`$SHELL\` error message, but got:\n\n${err.message}`); + } + }); + + // Does *not* warn when a global CLI install is present on the system. + await mockHome(async (home) => { + const { stdout } = await execWithEnv('ng', ['completion'], { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }); + + if (stdout.includes('there does not seem to be a global install of the Angular CLI')) { + throw new Error(`CLI warned about missing global install, but one should exist.`); + } + }); + + // Warns when a global CLI install is *not* present on the system. + await mockHome(async (home) => { + try { + // Temporarily uninstall the global CLI binary from the system. + await silentNpm(['uninstall', '--global', '@angular/cli', `--registry=${testRegistry}`]); + + // Setup a fake project directory with a local install of the CLI. + const projectDir = path.join(home, 'project'); + await fs.mkdir(projectDir); + await silentNpm(['init', '-y', `--registry=${testRegistry}`], { cwd: projectDir }); + await silentNpm(['install', '@angular/cli', `--registry=${testRegistry}`], { + cwd: projectDir, + }); + + // Invoke the local CLI binary. + const localCliBinary = path.join(projectDir, 'node_modules', '.bin', 'ng'); + const { stdout } = await execWithEnv(localCliBinary, ['completion'], { + ...process.env, + 'SHELL': '/usr/bin/zsh', + }); + + if (stdout.includes('there does not seem to be a global install of the Angular CLI')) { + throw new Error(`CLI warned about missing global install, but one should exist.`); + } + } finally { + // Reinstall global CLI for remainder of the tests. + await silentNpm(['install', '--global', '@angular/cli', `--registry=${testRegistry}`]); + } + }); +} + +async function windowsTests(): Promise { + // Should fail with a clear error message. + const err = await execAndCaptureError('ng', ['completion']); + if (!err.message.includes("Cmd and Powershell don't support command autocompletion")) { + throw new Error( + `Expected Windows autocompletion to fail with custom error, but got:\n\n${err.message}`, + ); + } +} diff --git a/tests/e2e/tests/commands/config/config-get.ts b/tests/e2e/tests/commands/config/config-get.ts new file mode 100644 index 000000000000..d64e0a630af0 --- /dev/null +++ b/tests/e2e/tests/commands/config/config-get.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { ng } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await expectToFail(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle')); + await ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'false'); + const { stdout } = await ng('config', 'schematics.@schematics/angular.component.inlineStyle'); + assert.match(stdout, /false\n?/); + + await ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'true'); + const { stdout: stdout1 } = await ng( + 'config', + 'schematics.@schematics/angular.component.inlineStyle', + ); + assert.match(stdout1, /true\n?/); + + await ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'false'); + const { stdout: stdout2 } = await ng( + 'config', + `projects.test-project.architect.build.options.assets[0]`, + ); + assert.ok(stdout2.includes('"input": "public"')); + + const { stdout: stdout3 } = await ng( + 'config', + `projects["test-project"].architect.build.options.assets[0]`, + ); + assert.ok(stdout3.includes('"input": "public"')); + + // should print all config when no positional args are provided. + const { stdout: stdout4 } = await ng('config'); + assert.ok(stdout4.includes('$schema')); +} diff --git a/tests/e2e/tests/commands/config/config-global-validation.ts b/tests/e2e/tests/commands/config/config-global-validation.ts new file mode 100644 index 000000000000..7be29130dca0 --- /dev/null +++ b/tests/e2e/tests/commands/config/config-global-validation.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { homedir } from 'node:os'; +import * as path from 'node:path'; +import { deleteFile, expectFileToExist } from '../../../utils/fs'; +import { ng, silentNg } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + let ngError: Error; + + ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted', 'true')); + assert.match( + ngError.message, + /Data path "\/cli" must NOT have additional properties\(completion\)\./, + ); + + ngError = await expectToFail(() => + silentNg('config', '--global', 'cli.completion.invalid', 'true'), + ); + assert.match( + ngError.message, + /Data path "\/cli\/completion" must NOT have additional properties\(invalid\)\./, + ); + + ngError = await expectToFail(() => silentNg('config', '--global', 'cli.cache.enabled', 'true')); + assert.match(ngError.message, /Data path "\/cli" must NOT have additional properties\(cache\)\./); + + ngError = await expectToFail(() => silentNg('config', 'cli.completion.prompted')); + assert.match(ngError.message, /Value cannot be found\./); + + await ng('config', '--global', 'cli.completion.prompted', 'true'); + const { stdout } = await silentNg('config', '--global', 'cli.completion.prompted'); + assert.match(stdout, /true/); + + await expectFileToExist(path.join(homedir(), '.angular-config.json')); + await deleteFile(path.join(homedir(), '.angular-config.json')); +} diff --git a/tests/e2e/tests/commands/config/config-global.ts b/tests/e2e/tests/commands/config/config-global.ts new file mode 100644 index 000000000000..030d4b583d2c --- /dev/null +++ b/tests/e2e/tests/commands/config/config-global.ts @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { homedir } from 'node:os'; +import * as path from 'node:path'; +import { deleteFile, expectFileToExist } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await expectToFail(() => + ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle'), + ); + + await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'false'); + let output = await ng( + 'config', + '--global', + 'schematics.@schematics/angular.component.inlineStyle', + ); + assert.match(output.stdout, /false\n?/); + + // This test requires schema querying capabilities + // .then(() => expectToFail(() => { + // return ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'INVALID_BOOLEAN'); + // })) + + const cwd = process.cwd(); + process.chdir('/'); + try { + await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'true'); + } finally { + process.chdir(cwd); + } + + output = await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle'); + assert.match(output.stdout, /true\n?/); + + await expectToFail(() => ng('config', '--global', 'cli.warnings.notreal', 'true')); + + await ng('config', '--global', 'cli.warnings.versionMismatch', 'false'); + await expectFileToExist(path.join(homedir(), '.angular-config.json')); + await deleteFile(path.join(homedir(), '.angular-config.json')); +} diff --git a/tests/e2e/tests/commands/config/config-set-enum-check.ts b/tests/e2e/tests/commands/config/config-set-enum-check.ts new file mode 100644 index 000000000000..571dc74cdd14 --- /dev/null +++ b/tests/e2e/tests/commands/config/config-set-enum-check.ts @@ -0,0 +1,15 @@ +import { ng } from '../../../utils/process'; + +export default async function () { + // These tests require schema querying capabilities + // .then(() => expectToFail( + // () => ng('config', 'schematics.@schematics/angular.component.aaa', 'bbb')), + // ) + // .then(() => expectToFail(() => ng( + // 'config', + // 'schematics.@schematics/angular.component.viewEncapsulation', + // 'bbb', + // ))) + + await ng('config', 'schematics.@schematics/angular.component.viewEncapsulation', 'Emulated'); +} diff --git a/tests/e2e/tests/commands/config/config-set-prefix.ts b/tests/e2e/tests/commands/config/config-set-prefix.ts new file mode 100644 index 000000000000..40f22699baed --- /dev/null +++ b/tests/e2e/tests/commands/config/config-set-prefix.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { ng } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default function () { + return Promise.resolve() + .then(() => expectToFail(() => ng('config', 'schematics.@schematics/angular.component.prefix'))) + .then(() => ng('config', 'schematics.@schematics/angular.component.prefix', 'new-prefix')) + .then(() => ng('config', 'schematics.@schematics/angular.component.prefix')) + .then(({ stdout }) => { + assert.match(stdout, /new-prefix/); + }); +} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-set-serve-port.ts b/tests/e2e/tests/commands/config/config-set-serve-port.ts similarity index 90% rename from tests/legacy-cli/e2e/tests/commands/config/config-set-serve-port.ts rename to tests/e2e/tests/commands/config/config-set-serve-port.ts index 7557c6f534c9..125d21d7e66f 100644 --- a/tests/legacy-cli/e2e/tests/commands/config/config-set-serve-port.ts +++ b/tests/e2e/tests/commands/config/config-set-serve-port.ts @@ -1,7 +1,7 @@ import { expectFileToMatch } from '../../../utils/fs'; import { ng } from '../../../utils/process'; -export default function() { +export default function () { return Promise.resolve() .then(() => ng('config', 'projects.test-project.architect.serve.options.port', '1234')) .then(() => expectFileToMatch('angular.json', /"port": 1234/)); diff --git a/tests/e2e/tests/commands/config/config-set.ts b/tests/e2e/tests/commands/config/config-set.ts new file mode 100644 index 000000000000..2152e573132e --- /dev/null +++ b/tests/e2e/tests/commands/config/config-set.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { ng, silentNg } from '../../../utils/process'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + let ngError: Error; + + ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz', 'true')); + assert.match( + ngError.message, + /Data path "\/cli\/warnings" must NOT have additional properties\(zzzz\)\./, + ); + + ngError = await expectToFail(() => silentNg('config', 'cli.warnings.zzzz')); + assert.match(ngError.message, /Value cannot be found\./); + + await ng('config', 'cli.warnings.versionMismatch', 'false'); + const { stdout } = await ng('config', 'cli.warnings.versionMismatch'); + assert.match(stdout, /false/); + + await ng('config', 'cli.packageManager', 'yarn'); + const { stdout: stdout2 } = await ng('config', 'cli.packageManager'); + assert.match(stdout2, /yarn/); + + await ng('config', 'schematics', '{"@schematics/angular:component":{"style": "scss"}}'); + const { stdout: stdout3 } = await ng('config', 'schematics.@schematics/angular:component.style'); + assert.match(stdout3, /scss/); + + await ng('config', 'schematics'); + await ng('config', 'schematics', 'undefined'); + await expectToFail(() => ng('config', 'schematics')); +} diff --git a/tests/e2e/tests/commands/help/help-hidden.ts b/tests/e2e/tests/commands/help/help-hidden.ts new file mode 100644 index 000000000000..bd972d26a942 --- /dev/null +++ b/tests/e2e/tests/commands/help/help-hidden.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { silentNg } from '../../../utils/process'; + +export default async function () { + const { stdout: stdoutNew } = await silentNg('--help'); + assert.doesNotMatch( + stdoutNew, + /(easter-egg)|(ng make-this-awesome)|(ng init)/, + 'Expected to not match "(easter-egg)|(ng make-this-awesome)|(ng init)" in help output.', + ); + + const { stdout: ngGenerate } = await silentNg('--help', 'generate', 'component'); + assert.doesNotMatch(ngGenerate, /--path/, 'Expected to not match "--path" in help output.'); +} diff --git a/tests/e2e/tests/commands/help/help-json.ts b/tests/e2e/tests/commands/help/help-json.ts new file mode 100644 index 000000000000..6f1b8e89db49 --- /dev/null +++ b/tests/e2e/tests/commands/help/help-json.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { silentNg } from '../../../utils/process'; + +export default async function () { + // This test is use as a sanity check. + const addHelpOutputSnapshot = JSON.stringify({ + 'name': 'config', + 'command': 'ng config [json-path] [value]', + 'shortDescription': + 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.', + 'longDescriptionRelativePath': '@angular/cli/src/commands/config/long-description.md', + 'longDescription': + 'A workspace has a single CLI configuration file, `angular.json`, at the top level.\nThe `projects` object contains a configuration object for each project in the workspace.\n\nYou can edit the configuration directly in a code editor,\nor indirectly on the command line using this command.\n\nThe configurable property names match command option names,\nexcept that in the configuration file, all names must use camelCase,\nwhile on the command line options can be given dash-case.\n\nFor further details, see [Workspace Configuration](reference/configs/workspace-config).\n\nFor configuration of CLI usage analytics, see [ng analytics](cli/analytics).\n', + 'options': [ + { + 'name': 'global', + 'type': 'boolean', + 'aliases': ['g'], + 'default': false, + 'description': "Access the global configuration in the caller's home directory.", + }, + { + 'name': 'help', + 'type': 'boolean', + 'description': 'Shows a help message for this command in the console.', + }, + { + 'name': 'json-path', + '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.', + 'positional': 0, + }, + { + 'name': 'value', + 'type': 'string', + 'description': 'If provided, a new value for the given configuration key.', + 'positional': 1, + }, + ], + }); + + const { stdout } = await silentNg('config', '--help', '--json-help'); + const output = JSON.stringify(JSON.parse(stdout.trim())); + + assert.strictEqual( + output, + addHelpOutputSnapshot, + `ng config JSON help output didn\'t match snapshot.`, + ); + + const { stdout: stdout2 } = await silentNg('--help', '--json-help'); + assert.doesNotThrow( + () => JSON.parse(stdout2.trim()), + `'ng --help ---json-help' failed to return JSON.`, + ); + + const { stdout: stdout3 } = await silentNg('generate', '--help', '--json-help'); + assert.doesNotThrow( + () => JSON.parse(stdout3.trim()), + `'ng generate --help ---json-help' failed to return JSON.`, + ); +} diff --git a/tests/e2e/tests/commands/ng-new-collection.ts b/tests/e2e/tests/commands/ng-new-collection.ts new file mode 100644 index 000000000000..e55c75ee69b4 --- /dev/null +++ b/tests/e2e/tests/commands/ng-new-collection.ts @@ -0,0 +1,19 @@ +import { execAndWaitForOutputToMatch } from '../../utils/process'; + +export default async function () { + const currentDirectory = process.cwd(); + + try { + process.chdir('..'); + + // The below is a way to validate that the `--collection` option is being considered. + await execAndWaitForOutputToMatch( + 'ng', + ['new', '--collection', 'invalid-schematic'], + /Collection "invalid-schematic" cannot be resolved/, + ); + } finally { + // Change directory back + process.chdir(currentDirectory); + } +} diff --git a/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts b/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts new file mode 100644 index 000000000000..b4b572bfa3ee --- /dev/null +++ b/tests/e2e/tests/commands/project-cannot-be-determined-by-cwd.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const errorMessage = + 'Cannot determine project for command.\n' + + 'This is a multi-project workspace and more than one project supports this command.'; + + // Delete root project + await updateJsonFile('angular.json', (workspaceJson) => { + delete workspaceJson.projects['test-project']; + }); + + await ng('generate', 'app', 'second-app', '--skip-install'); + await ng('generate', 'app', 'third-app', '--skip-install'); + + const startCwd = process.cwd(); + + try { + const { message } = await expectToFail(() => ng('build')); + assert.match(message, new RegExp(errorMessage)); + + // Help should still work + await execAndWaitForOutputToMatch('ng', ['build', '--help'], /--configuration/); + + // Yargs allows positional args to be passed as flags. Verify that in this case the project can be determined. + await ng('build', '--project=third-app', '--configuration=development'); + + process.chdir(join(startCwd, 'projects/second-app')); + await ng('build', '--configuration=development'); + } finally { + // Restore path + process.chdir(startCwd); + } +} diff --git a/tests/e2e/tests/commands/run-configuration-option.ts b/tests/e2e/tests/commands/run-configuration-option.ts new file mode 100644 index 000000000000..ab2761dce531 --- /dev/null +++ b/tests/e2e/tests/commands/run-configuration-option.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { silentNg } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const errorMatch = `Provide the configuration as part of the target 'ng run test-project:build:production`; + + { + const { message } = await expectToFail(() => + silentNg('run', 'test-project:build:development', '--configuration=production'), + ); + + assert.ok(message.includes(errorMatch)); + } + + { + const { message } = await expectToFail(() => + silentNg('run', 'test-project:build', '--configuration=production'), + ); + + assert.ok(message.includes(errorMatch)); + } +} diff --git a/tests/e2e/tests/commands/serve/assets.ts b/tests/e2e/tests/commands/serve/assets.ts new file mode 100644 index 000000000000..83fcb42aa8f2 --- /dev/null +++ b/tests/e2e/tests/commands/serve/assets.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert'; +import { randomUUID } from 'node:crypto'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { ngServe, updateJsonFile } from '../../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; + +export default async function () { + const outsideDirectoryName = `../outside-${randomUUID()}`; + + await updateJsonFile('angular.json', (json) => { + // Ensure assets located outside the workspace root work with the dev server + json.projects['test-project'].architect.build.options.assets.push({ + 'input': outsideDirectoryName, + 'glob': '**/*', + 'output': './outside', + }); + }); + + await mkdir(outsideDirectoryName); + try { + await writeFile(`${outsideDirectoryName}/some-asset.xyz`, 'XYZ'); + + const port = await ngServe(); + + let response = await fetch(`http://localhost:${port}/favicon.ico`); + assert.strictEqual(response.status, 200, 'favicon.ico response should be ok'); + + response = await fetch(`http://localhost:${port}/outside/some-asset.xyz`); + assert.strictEqual(response.status, 200, 'outside/some-asset.xyz response should be ok'); + assert.strictEqual(await response.text(), 'XYZ', 'outside/some-asset.xyz content is wrong'); + + // A non-existent HTML file request with accept header should fallback to the index HTML + response = await fetch(`http://localhost:${port}/does-not-exist.html`, { + headers: { accept: 'text/html' }, + }); + assert.strictEqual( + response.status, + 200, + 'non-existent file response should fallback and be ok', + ); + assert.match( + await response.text(), + / { + const result = await fetch(url, { method: 'HEAD' }); + const content = await result.blob(); + + assert.strictEqual(content.size, 0, `Expected "size" to be "0" but got "${content.size}".`); + assert.strictEqual( + result.status, + 200, + `Expected "status" to be "200" but got "${result.status}".`, + ); +} diff --git a/tests/e2e/tests/commands/serve/preflight-request.ts b/tests/e2e/tests/commands/serve/preflight-request.ts new file mode 100644 index 000000000000..02a763dce197 --- /dev/null +++ b/tests/e2e/tests/commands/serve/preflight-request.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { ngServe } from '../../../utils/project'; + +export default async function () { + const port = await ngServe(); + const result = await fetch(`http://localhost:${port}/main.js`, { method: 'OPTIONS' }); + const content = await result.blob(); + + assert.strictEqual(content.size, 0, `Expected "size" to be "0" but got "${content.size}".`); + assert.strictEqual( + result.status, + 204, + `Expected "status" to be "204" but got "${result.status}".`, + ); +} diff --git a/tests/e2e/tests/commands/serve/reload-shims.ts b/tests/e2e/tests/commands/serve/reload-shims.ts new file mode 100644 index 000000000000..345979da5cc2 --- /dev/null +++ b/tests/e2e/tests/commands/serve/reload-shims.ts @@ -0,0 +1,21 @@ +import { prependToFile, writeFile } from '../../../utils/fs'; +import { execAndWaitForOutputToMatch } from '../../../utils/process'; + +export default async function () { + // Simulate a JS library using a Node.js specific module + await writeFile('src/node-usage.js', `const path = require('path');\n`); + await prependToFile('src/main.ts', `import './node-usage';\n`); + + // Make sure serve is consistent with build + await execAndWaitForOutputToMatch( + 'ng', + ['build'], + /Module not found: Error: Can't resolve 'path'/, + ); + // The Node.js specific module should not be found + await execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port=0'], + /Module not found: Error: Can't resolve 'path'/, + ); +} diff --git a/tests/e2e/tests/commands/serve/serve-path.ts b/tests/e2e/tests/commands/serve/serve-path.ts new file mode 100644 index 000000000000..397a6c2f973f --- /dev/null +++ b/tests/e2e/tests/commands/serve/serve-path.ts @@ -0,0 +1,20 @@ +import * as assert from 'node:assert'; +import { ngServe } from '../../../utils/project'; + +export default async function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + const port = await ngServe('--serve-path', 'test/'); + + return Promise.resolve() + .then(() => fetch(`http://localhost:${port}/test`, { headers: { 'Accept': 'text/html' } })) + .then(async (response) => { + assert.strictEqual(response.status, 200); + assert.match(await response.text(), /<\/app-root>/); + }) + .then(() => fetch(`http://localhost:${port}/test/abc`, { headers: { 'Accept': 'text/html' } })) + .then(async (response) => { + assert.strictEqual(response.status, 200); + assert.match(await response.text(), /<\/app-root>/); + }); +} diff --git a/tests/e2e/tests/commands/serve/ssr-http-requests-assets.ts b/tests/e2e/tests/commands/serve/ssr-http-requests-assets.ts new file mode 100644 index 000000000000..19f1208646d6 --- /dev/null +++ b/tests/e2e/tests/commands/serve/ssr-http-requests-assets.ts @@ -0,0 +1,190 @@ +import assert from 'node:assert'; +import { Agent } from 'undici'; +import { killAllProcesses, ng } from '../../../utils/process'; +import { writeMultipleFiles } from '../../../utils/fs'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ngServe, useSha } from '../../../utils/project'; + +export default async function () { + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Add http client and route + 'src/app/app.config.ts': ` + import { ApplicationConfig } from '@angular/core'; + import { provideRouter } from '@angular/router'; + + import { Home } from './home/home'; + import { provideClientHydration } from '@angular/platform-browser'; + import { provideHttpClient, withFetch } from '@angular/common/http'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideRouter([{ + path: '', + component: Home, + }]), + provideClientHydration(), + provideHttpClient(withFetch()), + ], + }; + `, + // Add asset + 'public/media.json': JSON.stringify({ dataFromAssets: true }), + // Update component to do an HTTP call to asset. + 'src/app/app.ts': ` + import { ChangeDetectorRef, Component, inject } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + import { HttpClient } from '@angular/common/http'; + + @Component({ + selector: 'app-root', + imports: [CommonModule, RouterOutlet], + template: \` +

{{ data | json }}

+ + \`, + }) + export class App { + data: any; + private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + const http = inject(HttpClient); + http.get('/media.json').toPromise().then((d) => { + this.data = d; + this.cdr.markForCheck(); + }); + } + } + `, + }); + + await ng('generate', 'component', 'home'); + const match = /

{[\S\s]*"dataFromAssets":[\s\S]*true[\S\s]*}<\/p>/; + const port = await ngServe('--no-ssl'); + assert.match(await (await fetch(`http://localhost:${port}/`)).text(), match); + + await killAllProcesses(); + + const sslPort = await ngServe('--ssl'); + assert.match( + await ( + await fetch(`https://localhost:${sslPort}/`, { + dispatcher: new Agent({ + connect: { + rejectUnauthorized: false, + }, + }), + }) + ).text(), + match, + ); + + await killAllProcesses(); + + // With OpenSSl cert+key + writeMultipleFiles({ + 'server.key': `-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDOyBmVy61zEqfs +oTPQ9gTX233/nlrVXtaUGJkbDR5actq0X+XQtZuIoO4JgRpiYz5/8XiY8AiaMdt3 +0abugO5AhyIbsGyQxvz2si7yKQ+WUdF/DRTpfTq76E8EXR8W9BI+DTpG/1nNGBd1 +lpMa8NMfHqLvhtpebHuBcb1BCRely+FILHVDGf+dfCIvR0Zvt8Ah3qLL6vvX3pzF +qM9XrXfUgWKqpbz+L1BeCPILsH+UaOOCzzbyrLoY6fnawjUkc/ieHWycro7dBUlu +JJ/kcGtpPYD/hsMFcXz6D67qbIFuc6Dz9CrWIagAFMqK91FAtUpyfP7+jLikQOST +pFDLgJ+klADGCiZ6C/dvjUsYM+4ML9dX6Q9rj6sQo/oj0Dsdj39J5mVmklkbRP5q +heMGTyc09/ambiYFfzWEMMnEcCT/CS/r1q93oRDG02Cx6F1B05mtR+/AFxAJ6UxL +2u3oMPVY179FWjx2+YvbfrdNrFnWb8eRMiRZIW8O8ptKkrh+LL1Rhep+W/1Nxdrp +g7E2rWP8AWr3mdd+cnauvF/2yMecBDLVnk3OOSjcuLc+i9ngOD0xHdcRfO89mryj +IewEIrUQ4U0ZgyHMi99qV4wyXhd9HzTUgT01QofsiuF9xyVfnansQOj3oqOgCS92 +VEeqZnLXgaVoh/++/FV7r4C5zxLzLwIDAQABAoICAAeKSqD98iE3o5qc6AAiqj79 +r8L2dJ+0F9cDF4Bh6aLFYBGUoS/Sr38Cm7m0/3qiiEKvbpM9/0QVfHLRoBNcJnBk +0mrp1yD1tfEOUPcJ12D/3XJ2zlIv+7oUn97Ia9h4NCzBv5zw7lTsrjHenDMSZ7XD +PR6qb064XfiRETKFeCJk64Godj/3QkmX2FApCMDwXJttynLQseK5RZnDHojhuDuR +vgfC+aOCTit8GOkxi1Hdppxm8tmMwfqyJmAJh5IdKkNA3MHtbyPCxSXRRIUdwMXT +bhhVCh9/W3prv/vEYSPfRGs9WdtrTBj/U8GlgGlxa87h1i/i8N4I5RP+8lic6zVL +BIIPamkRFRNUmV7ZzpWsrLl1TUUcQJ1UsjNqaLD7jl+l0IaUta8I9crJQWIuQu+G +5C0XJQPZrqGkZfLSMvi08S+8myCzf+3P3ayUHAKz4Q1pTeM2BbHQi1HbT+WUsA5G +DD4xBwc8VJXOy0dB4v4e5eK8aZaJZroR5LJT7bvKw1MNpyAt6w2Z17eSSuEE0x6u +4uzOfHRaWiKH9gXVSKyo8xM08wiKAJIpDg4fDsu/XPjfPzV3eSHwin6ADw7rcOrW +j4Ca43Ts7Fz0Y40dtUyrrQ3f7WSQ9C+M88NuI9WYPWmXqPQY9+b5Au0Q8rq1j3dW +1YB3vYd6ElaLI6k7c5OhAoIBAQDt+Dgi2jrx2Xol0Per/cIFyG/hX/h+tavj++xl +gIMLLwhFmBVIkkXHjG5v5rZFCY7giQgdy+JHAIDUg3Ae3K7zSYidkMwQzLJ9udaT +nJEybY4RlEJZVBs58pkjevqTD/pZ+Kj09/VLAJIhOInFQHQ+ZVn4uHF+NO4tcsH+ +Wtsyyf8tFMkoNQ38o2oTnJtsotssKGdXCgi36BCCCUQk98113RK9dBTi+2iB59qr +WczAb6Jl5cs1j/2IC3z9KilZ3/ww4Bshs5LThIGR66KZIfApzf8XQzHM9mhiLgRU +thUZ0a/ougqf4FovLAezsNM7kYqbPDPOh/CayN5KZ8pHNLt1AoIBAQDecvFejv3u +Lm9kf2xRv06HTsLeEUSgRVoWdKidwW3tXOkl8vuBTzeFl9yrgtMBbSgcFASbEKPP +uPc6g+zkcakUB+FLGGNwNFKhdGPUMI7u8i9WeWH+e3Aios7n0tCPP0Xv6d1Lhcyw +X1nz07hZ+sT40nLGyf/6vfg8LFGSBrr3YQLseodKGTC9jc5yJqEX16cqHppkwaJT +Elsona7PZGFm/WFGWn4wZiPpd9P5lnxP+KrI+m84z4Gw5txcJsE8WiUrrQYHG3+2 +yeztwYl+JGHcspsU4WTPCupyVRHt0uuGVN+UhLKgER8wghc6fL08jGkHgVLrStnN +ekRA0gEZRzOTAoIBAGuQMheW2uPssGidfwXP6r5gbinKDnF/vpWLjrwGjbUlajDC +4IPwEfhzwot0Flk4S8u0ROXq/XmogZMNYkWg7LdtOoI2K/c//0ITGSmZsIvBt2C8 +ygzElpXn0U6XTOHia//1BLHNzqM7O9ImUyfEzYZSm4twG2S3mh0S7RsCiGf5pA0F +gzNYX90dJFp/BEXjivv3u1Y9Y9l03NlaROIM3GL1LX5TFQnQJ9noKhAfxAwLqbUz +XFn2ntu6jaGFSDGmq8CP29Os7qYLE+IYR2O+UmcjBLXIGp+RlXcjY7PCpeEIxeGF +Dj5b04fU+BpByAj57VPjr2sgSSI9vzSUm3r6G+0CggEBALK7JgZ028BxHN1hqHWy +QXVkKhxlQX+I2Y5rY0OFtD5gRZBRQBUwwgqb7xj7P3DI9M5Co0S4RPZUxogEkeUn +EdPfVPySdusjjzTcoI1QCrggbTqMwtjG811Q9O+9Kge+rgHLJRxWQBWCN3M6rMfX +PkYySThB+2PLGVW3wj6TG8xB7Sh2dpdp0AitlK+RLCRNCKpF9oV4M2WNvSLQNzG5 +lK08btkpQnS+zKH8vpuudumGgiqDVbQOvkSV6X49QUutnmoOVmaFiMMkUTLjKwbo +Up0SAJrxUp8sRR1iDsrIiqbfMNlTGXaU6zt9ew5qRV4N7yGxnh8hgAih8Y8nbOyT +kfMCggEBAMVOGy7yzaMQvVSkcFkUAnznI7RwvhUWusomfyaVgHm+j3Y7OPius1ah +4Da3kvb4t8OFUIdnMz/rte8xRKE6lKLNmXG9+8nPkCdrB9ovDr0Pw3ONZ7kKHuhm +75IKV72f3krZK5jJ88p/ruUUotButZb+WlGW5qQOJEJnHi65ABGYchAADAOBflXK +XbklHb6sVmEx6Ds4OMAbEmgH4C7BZuvmVeYMY7ihGIuBF3rE70rc2meQl/fxn0Gd ++/FrHDqCSkXwNT69HEOoLT/hi6Pc3kyn1bFOK+W8AydilI+6yOKkiYTSoCAO/yi/ +xlFXnn9FIQthAEWUhFgqApO+oKBn0hw= +-----END PRIVATE KEY----- +`, + 'server.crt': `-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUd0CiuFYYUTnnfB/Q6lijpEZJy4wwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTEwNzEwMTE0NFoXDTI2MTEw +NzEwMTE0NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAzsgZlcutcxKn7KEz0PYE19t9/55a1V7WlBiZGw0eWnLa +tF/l0LWbiKDuCYEaYmM+f/F4mPAImjHbd9Gm7oDuQIciG7BskMb89rIu8ikPllHR +fw0U6X06u+hPBF0fFvQSPg06Rv9ZzRgXdZaTGvDTHx6i74baXmx7gXG9QQkXpcvh +SCx1Qxn/nXwiL0dGb7fAId6iy+r7196cxajPV6131IFiqqW8/i9QXgjyC7B/lGjj +gs828qy6GOn52sI1JHP4nh1snK6O3QVJbiSf5HBraT2A/4bDBXF8+g+u6myBbnOg +8/Qq1iGoABTKivdRQLVKcnz+/oy4pEDkk6RQy4CfpJQAxgomegv3b41LGDPuDC/X +V+kPa4+rEKP6I9A7HY9/SeZlZpJZG0T+aoXjBk8nNPf2pm4mBX81hDDJxHAk/wkv +69avd6EQxtNgsehdQdOZrUfvwBcQCelMS9rt6DD1WNe/RVo8dvmL2363TaxZ1m/H +kTIkWSFvDvKbSpK4fiy9UYXqflv9TcXa6YOxNq1j/AFq95nXfnJ2rrxf9sjHnAQy +1Z5Nzjko3Li3PovZ4Dg9MR3XEXzvPZq8oyHsBCK1EOFNGYMhzIvfaleMMl4XfR80 +1IE9NUKH7IrhfcclX52p7EDo96KjoAkvdlRHqmZy14GlaIf/vvxVe6+Auc8S8y8C +AwEAAaNTMFEwHQYDVR0OBBYEFCOiC0xvMbfCFzmseoMDht+ydKBbMB8GA1UdIwQY +MBaAFCOiC0xvMbfCFzmseoMDht+ydKBbMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAJSiQwcaGVhwUorkb062cyyZOAstEJ5meg6H2g3nL894oWEU +FLc/S20z2tqO1It4rZB3cRKmB0RvH78eh4aUPAh0lPa/bm/h7WrgdEAJUmlNuZV3 +Hitd/c1d2OVzx6w+CFYd/G5GW3sWblYiH0paIN6s4TqHFY/IAzzZKQB7Ud7FJagM +KMkEP8RFDm7iRcENuSf51LtZb2NjN1TM5CK5sVXu62dvPYZC6SW052/qd1U+1Tyw +EX4fCqUgEoGoU6+Ftz3hCdVy3E4uzFBK1e5wmct6HULBZL51PWpf3BgwneZy0itE +lD6Y0H6m/9KMVcXpAHZK+6YnOOcWxIgfjykjZEO99rx3pVWPw1uSBUJEu1SLknAn +JDe+WLp+xmB8s62EjixZsEGqoQYYrtZ3vz8u4PSSgYPJjdAkFdLOPitf0U8ZW9/7 +hGyHgqd7WQ3toBwwdnPo6fZqHHyN8rXeWcmx8Uj9oyY1uunkSmq3csITPQg/zKBO +6RsO3pPj8mHjeAZCDs+Ks68ccPsn+53fJ9CrjiJlHFIP0ywbEBO1snJDit5q3gQI +/UpClB9Sl+mz4wznOvrKycrxrLEptZeBA5c6M9Qr30YJAb/prxvzSY5FrUGcstkO +CQVzSwZEUXxSo6K4cJ55vC0p3P3aoMvEpHfM+JqL3lCM9qWrxfkhvn8YS+Gg +-----END CERTIFICATE----- +`, + }); + + const sslPortForCerts = await ngServe('--ssl', '--ssl-cert=server.crt', '--ssl-key="server.key"'); + assert.match( + await ( + await fetch(`https://localhost:${sslPortForCerts}/`, { + dispatcher: new Agent({ + connect: { + rejectUnauthorized: false, + }, + }), + }) + ).text(), + match, + ); +} diff --git a/tests/e2e/tests/commands/unknown-configuration.ts b/tests/e2e/tests/commands/unknown-configuration.ts new file mode 100644 index 000000000000..c74d6547de59 --- /dev/null +++ b/tests/e2e/tests/commands/unknown-configuration.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const error = await expectToFail(() => ng('build', '--configuration', 'invalid')); + assert.match( + error.message, + /Configuration 'invalid' for target 'build' in project 'test-project' is not set in the workspace/, + ); +} diff --git a/tests/e2e/tests/commands/unknown-option.ts b/tests/e2e/tests/commands/unknown-option.ts new file mode 100644 index 000000000000..f0f4cde0693f --- /dev/null +++ b/tests/e2e/tests/commands/unknown-option.ts @@ -0,0 +1,17 @@ +import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + await expectToFail(() => ng('build', '--notanoption')); + + await execAndWaitForOutputToMatch( + 'ng', + ['build', '--notanoption'], + /Unknown argument: notanoption/, + ); + + const ngGenerateArgs = ['generate', 'component', 'component-name', '--notanoption']; + await expectToFail(() => ng(...ngGenerateArgs)); + + await execAndWaitForOutputToMatch('ng', ngGenerateArgs, /Unknown argument: notanoption/); +} diff --git a/tests/e2e/tests/generate/application/application-no-zoneless-ng-module.ts b/tests/e2e/tests/generate/application/application-no-zoneless-ng-module.ts new file mode 100644 index 000000000000..b475ed03b3a7 --- /dev/null +++ b/tests/e2e/tests/generate/application/application-no-zoneless-ng-module.ts @@ -0,0 +1,16 @@ +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { useCIChrome, useSha } from '../../../utils/project'; + +export default async function () { + try { + await ng('generate', 'app', 'ngmodules', '--no-standalone', '--skip-install', '--no-zoneless'); + await useSha(); + await installWorkspacePackages(); + await useCIChrome('ngmodules', 'projects/ngmodules'); + await ng('test', 'ngmodules', '--watch=false'); + await ng('build', 'ngmodules'); + } finally { + await uninstallPackage('zone.js'); + } +} diff --git a/tests/e2e/tests/generate/application/application-no-zoneless-standalone.ts b/tests/e2e/tests/generate/application/application-no-zoneless-standalone.ts new file mode 100644 index 000000000000..065957f9fe11 --- /dev/null +++ b/tests/e2e/tests/generate/application/application-no-zoneless-standalone.ts @@ -0,0 +1,16 @@ +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { useCIChrome, useSha } from '../../../utils/project'; + +export default async function () { + try { + await ng('generate', 'app', 'standalone', '--standalone', '--skip-install', '--no-zoneless'); + await useSha(); + await installWorkspacePackages(); + await useCIChrome('standalone', 'projects/standalone'); + await ng('test', 'standalone', '--watch=false'); + await ng('build', 'standalone'); + } finally { + await uninstallPackage('zone.js'); + } +} diff --git a/tests/e2e/tests/generate/application/application-zoneless-ng-module.ts b/tests/e2e/tests/generate/application/application-zoneless-ng-module.ts new file mode 100644 index 000000000000..5ad207a2fa76 --- /dev/null +++ b/tests/e2e/tests/generate/application/application-zoneless-ng-module.ts @@ -0,0 +1,12 @@ +import { installWorkspacePackages } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { useCIChrome, useSha } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'app', 'ngmodules', '--no-standalone', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await useCIChrome('ngmodules', 'projects/ngmodules'); + await ng('test', 'ngmodules', '--watch=false'); + await ng('build', 'ngmodules'); +} diff --git a/tests/e2e/tests/generate/application/application-zoneless-standalone.ts b/tests/e2e/tests/generate/application/application-zoneless-standalone.ts new file mode 100644 index 000000000000..b203ed4a28d0 --- /dev/null +++ b/tests/e2e/tests/generate/application/application-zoneless-standalone.ts @@ -0,0 +1,12 @@ +import { installWorkspacePackages } from '../../../utils/packages'; +import { ng } from '../../../utils/process'; +import { useCIChrome, useSha } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'app', 'standalone', '--standalone', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await useCIChrome('standalone', 'projects/standalone'); + await ng('test', 'standalone', '--watch=false'); + await ng('build', 'standalone'); +} diff --git a/tests/e2e/tests/generate/component/component-basic.ts b/tests/e2e/tests/generate/component/component-basic.ts new file mode 100644 index 000000000000..b87e6acad2bf --- /dev/null +++ b/tests/e2e/tests/generate/component/component-basic.ts @@ -0,0 +1,20 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; + +export default function () { + const projectDir = join('src', 'app'); + const componentDir = join(projectDir, 'test-component'); + + return ( + ng('generate', 'component', 'test-component') + .then(() => expectFileToExist(componentDir)) + .then(() => expectFileToExist(join(componentDir, 'test-component.ts'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.spec.ts'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.html'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.css'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-child-dir.ts b/tests/e2e/tests/generate/component/component-child-dir.ts similarity index 91% rename from tests/legacy-cli/e2e/tests/generate/component/component-child-dir.ts rename to tests/e2e/tests/generate/component/component-child-dir.ts index aa711551e9df..6c7dc7506bab 100644 --- a/tests/legacy-cli/e2e/tests/generate/component/component-child-dir.ts +++ b/tests/e2e/tests/generate/component/component-child-dir.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import { join } from 'node:path'; import { ng } from '../../../utils/process'; import { createDir, expectFileToExist, rimraf } from '../../../utils/fs'; @@ -19,10 +19,10 @@ export default async function () { // Ensure component is created in the correct location relative to the workspace root const componentDirectory = join(childDirectory, 'test-component'); - await expectFileToExist(join(componentDirectory, 'test-component.component.ts')); - await expectFileToExist(join(componentDirectory, 'test-component.component.spec.ts')); - await expectFileToExist(join(componentDirectory, 'test-component.component.html')); - await expectFileToExist(join(componentDirectory, 'test-component.component.css')); + await expectFileToExist(join(componentDirectory, 'test-component.ts')); + await expectFileToExist(join(componentDirectory, 'test-component.spec.ts')); + await expectFileToExist(join(componentDirectory, 'test-component.html')); + await expectFileToExist(join(componentDirectory, 'test-component.css')); // Ensure unit test execute and pass await ng('test', '--watch=false'); diff --git a/tests/e2e/tests/generate/component/component-flat.ts b/tests/e2e/tests/generate/component/component-flat.ts new file mode 100644 index 000000000000..4bdb101391fa --- /dev/null +++ b/tests/e2e/tests/generate/component/component-flat.ts @@ -0,0 +1,27 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; +import { updateJsonFile } from '../../../utils/project'; + +export default function () { + const appDir = join('src', 'app'); + return ( + Promise.resolve() + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].schematics = { + '@schematics/angular:component': { flat: true }, + }; + }), + ) + .then(() => ng('generate', 'component', 'test-component')) + .then(() => expectFileToExist(appDir)) + .then(() => expectFileToExist(join(appDir, 'test-component.ts'))) + .then(() => expectFileToExist(join(appDir, 'test-component.spec.ts'))) + .then(() => expectFileToExist(join(appDir, 'test-component.html'))) + .then(() => expectFileToExist(join(appDir, 'test-component.css'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/component/component-inline-template.ts b/tests/e2e/tests/generate/component/component-inline-template.ts new file mode 100644 index 000000000000..135b97c43fcb --- /dev/null +++ b/tests/e2e/tests/generate/component/component-inline-template.ts @@ -0,0 +1,29 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; +import { updateJsonFile } from '../../../utils/project'; +import { expectToFail } from '../../../utils/utils'; + +// tslint:disable:max-line-length +export default function () { + const componentDir = join('src', 'app', 'test-component'); + return ( + Promise.resolve() + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].schematics = { + '@schematics/angular:component': { inlineTemplate: true }, + }; + }), + ) + .then(() => ng('generate', 'component', 'test-component')) + .then(() => expectFileToExist(componentDir)) + .then(() => expectFileToExist(join(componentDir, 'test-component.ts'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.spec.ts'))) + .then(() => expectToFail(() => expectFileToExist(join(componentDir, 'test-component.html')))) + .then(() => expectFileToExist(join(componentDir, 'test-component.css'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/component/component-not-flat.ts b/tests/e2e/tests/generate/component/component-not-flat.ts new file mode 100644 index 000000000000..eedc3926da89 --- /dev/null +++ b/tests/e2e/tests/generate/component/component-not-flat.ts @@ -0,0 +1,28 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; +import { updateJsonFile } from '../../../utils/project'; + +export default function () { + const componentDir = join('src', 'app', 'test-component'); + + return ( + Promise.resolve() + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].schematics = { + '@schematics/angular:component': { flat: false }, + }; + }), + ) + .then(() => ng('generate', 'component', 'test-component')) + .then(() => expectFileToExist(componentDir)) + .then(() => expectFileToExist(join(componentDir, 'test-component.ts'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.spec.ts'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.html'))) + .then(() => expectFileToExist(join(componentDir, 'test-component.css'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-path-case.ts b/tests/e2e/tests/generate/component/component-path-case.ts similarity index 85% rename from tests/legacy-cli/e2e/tests/generate/component/component-path-case.ts rename to tests/e2e/tests/generate/component/component-path-case.ts index f76c09abe1fc..63d08af7099d 100644 --- a/tests/legacy-cli/e2e/tests/generate/component/component-path-case.ts +++ b/tests/e2e/tests/generate/component/component-path-case.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import { join } from 'node:path'; import { ng } from '../../../utils/process'; import { expectFileToExist, rimraf } from '../../../utils/fs'; @@ -11,22 +11,22 @@ export default async function () { try { // Generate a component - await ng('generate', 'component', `${upperDirs}/test-component`) + await ng('generate', 'component', `${upperDirs}/test-component`); // Ensure component is created in the correct location relative to the workspace root - await expectFileToExist(join(componentDirectory, 'test-component.component.ts')); - await expectFileToExist(join(componentDirectory, 'test-component.component.spec.ts')); - await expectFileToExist(join(componentDirectory, 'test-component.component.html')); - await expectFileToExist(join(componentDirectory, 'test-component.component.css')); + await expectFileToExist(join(componentDirectory, 'test-component.ts')); + await expectFileToExist(join(componentDirectory, 'test-component.spec.ts')); + await expectFileToExist(join(componentDirectory, 'test-component.html')); + await expectFileToExist(join(componentDirectory, 'test-component.css')); // Generate another component await ng('generate', 'component', `${upperDirs}/Test-Component-Two`); // Ensure component is created in the correct location relative to the workspace root - await expectFileToExist(join(componentTwoDirectory, 'test-component-two.component.ts')); - await expectFileToExist(join(componentTwoDirectory, 'test-component-two.component.spec.ts')); - await expectFileToExist(join(componentTwoDirectory, 'test-component-two.component.html')); - await expectFileToExist(join(componentTwoDirectory, 'test-component-two.component.css')); + await expectFileToExist(join(componentTwoDirectory, 'test-component-two.ts')); + await expectFileToExist(join(componentTwoDirectory, 'test-component-two.spec.ts')); + await expectFileToExist(join(componentTwoDirectory, 'test-component-two.html')); + await expectFileToExist(join(componentTwoDirectory, 'test-component-two.css')); // Ensure unit test execute and pass await ng('test', '--watch=false'); diff --git a/tests/e2e/tests/generate/component/component-prefix.ts b/tests/e2e/tests/generate/component/component-prefix.ts new file mode 100644 index 000000000000..befa96939b00 --- /dev/null +++ b/tests/e2e/tests/generate/component/component-prefix.ts @@ -0,0 +1,27 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToMatch } from '../../../utils/fs'; +import { updateJsonFile } from '../../../utils/project'; + +export default function () { + const testCompDir = join('src', 'app', 'test-component'); + const aliasCompDir = join('src', 'app', 'alias'); + + return ( + Promise.resolve() + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].schematics = { + '@schematics/angular:component': { prefix: 'pre' }, + }; + }), + ) + .then(() => ng('generate', 'component', 'test-component')) + .then(() => expectFileToMatch(join(testCompDir, 'test-component.ts'), /selector: 'pre-/)) + .then(() => ng('g', 'c', 'alias')) + .then(() => expectFileToMatch(join(aliasCompDir, 'alias.ts'), /selector: 'pre-/)) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/config/type-browserslist.ts b/tests/e2e/tests/generate/config/type-browserslist.ts new file mode 100644 index 000000000000..b9717852a4f1 --- /dev/null +++ b/tests/e2e/tests/generate/config/type-browserslist.ts @@ -0,0 +1,6 @@ +import { ng } from '../../../utils/process'; + +export default async function () { + await ng('generate', 'config', 'browserslist'); + await ng('build'); +} diff --git a/tests/e2e/tests/generate/config/type-karma.ts b/tests/e2e/tests/generate/config/type-karma.ts new file mode 100644 index 000000000000..c81b7d6b1abf --- /dev/null +++ b/tests/e2e/tests/generate/config/type-karma.ts @@ -0,0 +1,8 @@ +import { ng } from '../../../utils/process'; +import { useCIChrome } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'config', 'karma'); + await useCIChrome('test-project'); + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/generate/directive/directive-basic.ts b/tests/e2e/tests/generate/directive/directive-basic.ts new file mode 100644 index 000000000000..9ad00dfa22a3 --- /dev/null +++ b/tests/e2e/tests/generate/directive/directive-basic.ts @@ -0,0 +1,15 @@ +import { ng } from '../../../utils/process'; +import { join } from 'node:path'; +import { expectFileToExist } from '../../../utils/fs'; + +export default function () { + const directiveDir = join('src', 'app'); + return ( + ng('generate', 'directive', 'test-directive') + .then(() => expectFileToExist(join(directiveDir, 'test-directive.ts'))) + .then(() => expectFileToExist(join(directiveDir, 'test-directive.spec.ts'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/directive/directive-prefix.ts b/tests/e2e/tests/generate/directive/directive-prefix.ts new file mode 100644 index 000000000000..a8b5981b34a3 --- /dev/null +++ b/tests/e2e/tests/generate/directive/directive-prefix.ts @@ -0,0 +1,46 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToMatch } from '../../../utils/fs'; +import { updateJsonFile, useCIChrome, useCIDefaults } from '../../../utils/project'; + +export default function () { + const directiveDir = join('src', 'app'); + + return ( + Promise.resolve() + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.schematics = { + '@schematics/angular:directive': { prefix: 'preW' }, + }; + }), + ) + .then(() => ng('generate', 'directive', 'test2-directive')) + .then(() => expectFileToMatch(join(directiveDir, 'test2-directive.ts'), /selector: '\[preW/)) + .then(() => + ng('generate', 'application', 'app-two', '--skip-install', '--test-runner', 'karma'), + ) + .then(() => useCIDefaults('app-two')) + .then(() => useCIChrome('app-two', './projects/app-two')) + .then(() => + updateJsonFile('angular.json', (configJson) => { + configJson.projects['test-project'].schematics = { + '@schematics/angular:directive': { prefix: 'preP' }, + }; + }), + ) + .then(() => process.chdir('projects/app-two')) + .then(() => ng('generate', 'directive', '--skip-import', 'test3-directive')) + .then(() => process.chdir('../..')) + .then(() => + expectFileToMatch(join('projects', 'app-two', 'test3-directive.ts'), /selector: '\[preW/), + ) + .then(() => process.chdir('src/app')) + .then(() => ng('generate', 'directive', 'test-directive')) + .then(() => process.chdir('../..')) + .then(() => expectFileToMatch(join(directiveDir, 'test-directive.ts'), /selector: '\[preP/)) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/generate-error.ts b/tests/e2e/tests/generate/generate-error.ts new file mode 100644 index 000000000000..cee6d7998239 --- /dev/null +++ b/tests/e2e/tests/generate/generate-error.ts @@ -0,0 +1,9 @@ +import { ng } from '../../utils/process'; +import { deleteFile } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + return deleteFile('angular.json').then(() => + expectToFail(() => ng('generate', 'class', 'hello')), + ); +} diff --git a/tests/e2e/tests/generate/generate-name-check.ts b/tests/e2e/tests/generate/generate-name-check.ts new file mode 100644 index 000000000000..6fe89a58face --- /dev/null +++ b/tests/e2e/tests/generate/generate-name-check.ts @@ -0,0 +1,27 @@ +import { join } from 'node:path'; +import { ng } from '../../utils/process'; +import { expectFileToExist } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; + +export default function () { + const compDir = join('src', 'app', 'test-component'); + + return ( + Promise.resolve() + .then(() => + updateJsonFile('package.json', (configJson) => { + delete configJson.name; + return configJson; + }), + ) + .then(() => ng('generate', 'component', 'test-component')) + .then(() => expectFileToExist(compDir)) + .then(() => expectFileToExist(join(compDir, 'test-component.ts'))) + .then(() => expectFileToExist(join(compDir, 'test-component.spec.ts'))) + .then(() => expectFileToExist(join(compDir, 'test-component.html'))) + .then(() => expectFileToExist(join(compDir, 'test-component.css'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/guard/guard-basic.ts b/tests/e2e/tests/generate/guard/guard-basic.ts new file mode 100644 index 000000000000..ca4e9a547ff6 --- /dev/null +++ b/tests/e2e/tests/generate/guard/guard-basic.ts @@ -0,0 +1,15 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist, expectFileToMatch } from '../../../utils/fs'; + +export default async function () { + // Does not create a sub directory. + const guardDir = join('src', 'app'); + + await ng('generate', 'guard', 'test'); + await expectFileToExist(guardDir); + await expectFileToExist(join(guardDir, 'test-guard.ts')); + await expectFileToMatch(join(guardDir, 'test-guard.ts'), /export const testGuard: CanActivateFn/); + await expectFileToExist(join(guardDir, 'test-guard.spec.ts')); + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/generate/guard/guard-implements.ts b/tests/e2e/tests/generate/guard/guard-implements.ts new file mode 100644 index 000000000000..ca7c35f754a4 --- /dev/null +++ b/tests/e2e/tests/generate/guard/guard-implements.ts @@ -0,0 +1,15 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist, expectFileToMatch } from '../../../utils/fs'; + +export default async function () { + // Does not create a sub directory. + const guardDir = join('src', 'app'); + + await ng('generate', 'guard', 'match', '--implements=CanMatch'); + await expectFileToExist(guardDir); + await expectFileToExist(join(guardDir, 'match-guard.ts')); + await expectFileToMatch(join(guardDir, 'match-guard.ts'), /export const matchGuard: CanMatch/); + await expectFileToExist(join(guardDir, 'match-guard.spec.ts')); + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/generate/guard/guard-multiple-implements.ts b/tests/e2e/tests/generate/guard/guard-multiple-implements.ts new file mode 100644 index 000000000000..4359eaaf9f59 --- /dev/null +++ b/tests/e2e/tests/generate/guard/guard-multiple-implements.ts @@ -0,0 +1,26 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist, expectFileToMatch } from '../../../utils/fs'; + +export default async function () { + // Does not create a sub directory. + const guardDir = join('src', 'app'); + + // multiple implements are only supported in (deprecated) class-based guards + await ng( + 'generate', + 'guard', + 'multiple', + '--implements=CanActivate', + '--implements=CanDeactivate', + '--no-functional', + ); + await expectFileToExist(guardDir); + await expectFileToExist(join(guardDir, 'multiple-guard.ts')); + await expectFileToMatch( + join(guardDir, 'multiple-guard.ts'), + /implements CanActivate, CanDeactivate/, + ); + await expectFileToExist(join(guardDir, 'multiple-guard.spec.ts')); + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/generate/help-output-no-duplicates.ts b/tests/e2e/tests/generate/help-output-no-duplicates.ts new file mode 100644 index 000000000000..0be6ea93ea48 --- /dev/null +++ b/tests/e2e/tests/generate/help-output-no-duplicates.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import { ng } from '../../utils/process'; + +export default async function () { + // Verify that there are no duplicate options + const { stdout } = await ng('generate', 'component', '--help'); + const firstIndex = stdout.indexOf('--prefix'); + + assert.ok(firstIndex >= 0, '--prefix was not part of the help output.'); + assert.strictEqual( + firstIndex, + stdout.lastIndexOf('--prefix'), + '--prefix first and last index were different. Possible duplicate output!', + ); +} diff --git a/tests/e2e/tests/generate/help-output.ts b/tests/e2e/tests/generate/help-output.ts new file mode 100644 index 000000000000..86fc3b988b38 --- /dev/null +++ b/tests/e2e/tests/generate/help-output.ts @@ -0,0 +1,122 @@ +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { createDir, writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default function () { + // setup temp collection + const genRoot = join('node_modules/fake-schematics/'); + + return ( + Promise.resolve() + .then(() => createDir(genRoot)) + .then(() => + writeMultipleFiles({ + [join(genRoot, 'package.json')]: ` + { + "schematics": "./collection.json" + }`, + [join(genRoot, 'collection.json')]: ` + { + "schematics": { + "fake": { + "factory": "./fake", + "description": "Fake schematic", + "schema": "./fake-schema.json" + }, + } + }`, + [join(genRoot, 'fake-schema.json')]: ` + { + "$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" + } + } + }`, + [join(genRoot, 'fake.js')]: ` + function def(options) { + return (host, context) => { + return host; + }; + } + exports.default = def; + `, + }), + ) + .then(() => ng('generate', 'fake-schematics:fake', '--help')) + .then(({ stdout }) => { + assert.match(stdout, /ng generate fake-schematics:fake \[b\]/); + assert.match(stdout, /opt-a[\s\S]*opt-b[\s\S]*opt-c/); + }) + // set up default collection. + .then(() => + updateJsonFile('angular.json', (json) => { + json.cli = json.cli || ({} as any); + json.cli.schematicCollections = ['fake-schematics']; + }), + ) + .then(() => ng('generate', 'fake', '--help')) + // verify same output + .then(({ stdout }) => { + assert.match(stdout, /ng generate fake \[b\]/); + assert.match(stdout, /opt-a[\s\S]*opt-b[\s\S]*opt-c/); + }) + + // should print all the available schematics in a collection + // when a collection has more than 1 schematic + .then(() => + writeMultipleFiles({ + [join(genRoot, 'collection.json')]: ` + { + "schematics": { + "fake": { + "factory": "./fake", + "description": "Fake schematic", + "schema": "./fake-schema.json" + }, + "fake-two": { + "factory": "./fake", + "description": "Fake schematic", + "schema": "./fake-schema.json" + }, + } + }`, + }), + ) + .then(() => ng('generate', '--help')) + .then(({ stdout }) => { + assert.match(stdout, /fake[\s\S]*fake-two/); + }) + ); +} diff --git a/tests/e2e/tests/generate/install-allow-scripts.ts b/tests/e2e/tests/generate/install-allow-scripts.ts new file mode 100644 index 000000000000..f41a49935d80 --- /dev/null +++ b/tests/e2e/tests/generate/install-allow-scripts.ts @@ -0,0 +1,31 @@ +import { copyAssets } from '../../utils/assets'; +import { expectFileNotToExist, expectFileToExist, rimraf } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + // Copy test schematic into test project to ensure schematic dependencies are available + await copyAssets('schematic-allow-scripts', 'schematic-allow-scripts'); + + // By default should not run the postinstall from the added package.json in the schematic + await ng('generate', './schematic-allow-scripts:test'); + await expectFileToExist('install-test/package.json'); + await expectFileNotToExist('install-test/post-script-ran'); + + // Cleanup for next test case + await rimraf('install-test'); + + // Should run the postinstall if the allowScripts task option is enabled + // For testing purposes, this schematic exposes the task option via a schematic option + await ng('generate', './schematic-allow-scripts:test', '--allow-scripts'); + await expectFileToExist('install-test/package.json'); + await expectFileToExist('install-test/post-script-ran'); + + // Cleanup for next test case + await rimraf('install-test'); + + // Package manager configuration should take priority + // The `ignoreScripts` schematic option sets the value of the `ignore-scripts` option in a test project `.npmrc` + await ng('generate', './schematic-allow-scripts:test', '--allow-scripts', '--ignore-scripts'); + await expectFileToExist('install-test/package.json'); + await expectFileNotToExist('install-test/post-script-ran'); +} diff --git a/tests/e2e/tests/generate/interceptor/interceptor-basic.ts b/tests/e2e/tests/generate/interceptor/interceptor-basic.ts new file mode 100755 index 000000000000..7dd0b48c439b --- /dev/null +++ b/tests/e2e/tests/generate/interceptor/interceptor-basic.ts @@ -0,0 +1,18 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; + +export default function () { + // Does not create a sub directory. + const interceptorDir = join('src', 'app'); + + return ( + ng('generate', 'interceptor', 'test') + .then(() => expectFileToExist(interceptorDir)) + .then(() => expectFileToExist(join(interceptorDir, 'test-interceptor.ts'))) + .then(() => expectFileToExist(join(interceptorDir, 'test-interceptor.spec.ts'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/library/library-basic.ts b/tests/e2e/tests/generate/library/library-basic.ts new file mode 100644 index 000000000000..efc74694ea00 --- /dev/null +++ b/tests/e2e/tests/generate/library/library-basic.ts @@ -0,0 +1,9 @@ +import { ng } from '../../../utils/process'; +import { useCIChrome } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'library', 'lib-ngmodule', '--no-standalone'); + await useCIChrome('lib-ngmodule', 'projects/lib-ngmodule'); + await ng('test', 'lib-ngmodule', '--no-watch'); + await ng('build', 'lib-ngmodule'); +} diff --git a/tests/e2e/tests/generate/library/library-standalone.ts b/tests/e2e/tests/generate/library/library-standalone.ts new file mode 100644 index 000000000000..0b9754644bf5 --- /dev/null +++ b/tests/e2e/tests/generate/library/library-standalone.ts @@ -0,0 +1,9 @@ +import { ng } from '../../../utils/process'; +import { useCIChrome } from '../../../utils/project'; + +export default async function () { + await ng('generate', 'library', 'lib-standalone', '--standalone'); + await useCIChrome('lib-standalone', 'projects/lib-standalone'); + await ng('test', 'lib-standalone', '--no-watch'); + await ng('build', 'lib-standalone'); +} diff --git a/tests/e2e/tests/generate/module/module-basic.ts b/tests/e2e/tests/generate/module/module-basic.ts new file mode 100644 index 000000000000..18e1a05fedde --- /dev/null +++ b/tests/e2e/tests/generate/module/module-basic.ts @@ -0,0 +1,23 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist, expectFileToMatch } from '../../../utils/fs'; +import { expectToFail } from '../../../utils/utils'; +import { useCIChrome, useCIDefaults } from '../../../utils/project'; + +export default async function () { + const projectName = 'test-project-two'; + const moduleDir = `projects/${projectName}/src/app/test`; + await ng('generate', 'application', projectName, '--no-standalone', '--skip-install'); + await useCIDefaults(projectName); + await useCIChrome(projectName, 'projects/test-project-two'); + + await ng('generate', 'module', 'test', '--project', projectName); + await expectFileToExist(moduleDir); + await expectFileToExist(join(moduleDir, 'test-module.ts')); + await expectToFail(() => expectFileToExist(join(moduleDir, 'test-routing-module.ts'))); + await expectToFail(() => expectFileToExist(join(moduleDir, 'test.spec.ts'))); + await expectFileToMatch(join(moduleDir, 'test-module.ts'), 'TestModule'); + + // Try to run the unit tests. + await ng('test', projectName, '--watch=false'); +} diff --git a/tests/e2e/tests/generate/module/module-import.ts b/tests/e2e/tests/generate/module/module-import.ts new file mode 100644 index 000000000000..533b3d19efe7 --- /dev/null +++ b/tests/e2e/tests/generate/module/module-import.ts @@ -0,0 +1,59 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToMatch } from '../../../utils/fs'; + +export default async function () { + const projectName = 'test-project-two'; + await ng('generate', 'application', projectName, '--no-standalone', '--skip-install'); + await ng('generate', 'module', 'sub', '--project', projectName); + await ng('generate', 'module', 'sub/deep', '--project', projectName); + + const projectAppDir = `projects/${projectName}/src/app`; + const modulePath = join(projectAppDir, 'app-module.ts'); + const subModulePath = join(projectAppDir, 'sub/sub-module.ts'); + const deepSubModulePath = join(projectAppDir, 'sub/deep/deep-module.ts'); + + await ng('generate', 'module', 'test1', '--module', 'app-module.ts', '--project', projectName); + await expectFileToMatch(modulePath, `import { Test1Module } from './test1/test1-module'`); + await expectFileToMatch(modulePath, /imports: \[.*?Test1Module.*?\]/s); + + await ng('generate', 'module', 'test2', '--module', 'app-module', '--project', projectName); + await expectFileToMatch(modulePath, `import { Test2Module } from './test2/test2-module'`); + await expectFileToMatch(modulePath, /imports: \[.*?Test2Module.*?\]/s); + + await ng('generate', 'module', 'test3', '--module', 'app', '--project', projectName); + await expectFileToMatch(modulePath, `import { Test3Module } from './test3/test3-module'`); + await expectFileToMatch(modulePath, /imports: \[.*?Test3Module.*?\]/s); + + await ng('generate', 'module', 'test4', '--routing', '--module', 'app', '--project', projectName); + await expectFileToMatch(modulePath, /imports: \[.*?Test4Module.*?\]/s); + await expectFileToMatch( + join(projectAppDir, 'test4/test4-module.ts'), + `import { Test4RoutingModule } from './test4-routing-module'`, + ); + await expectFileToMatch( + join(projectAppDir, 'test4/test4-module.ts'), + /imports: \[.*?Test4RoutingModule.*?\]/s, + ); + + await ng('generate', 'module', 'test5', '--module', 'sub', '--project', projectName); + await expectFileToMatch(subModulePath, `import { Test5Module } from '../test5/test5-module'`); + + await expectFileToMatch(subModulePath, /imports: \[.*?Test5Module.*?\]/s); + + await ng('generate', 'module', 'test6', '--module', 'sub/deep', '--project', projectName); + + await expectFileToMatch( + deepSubModulePath, + `import { Test6Module } from '../../test6/test6-module'`, + ); + await expectFileToMatch(deepSubModulePath, /imports: \[.*?Test6Module.*?\]/s); + + // E2E_DISABLE: temporarily disable pending investigation + // await process.chdir(join(root, 'src', 'app'))) + // await ng('generate', 'module', 'test7', '--module', 'app-module.ts')) + // await process.chdir('..')) + // await expectFileToMatch(modulePath, + // /import { Test7Module } from '.\/test7\/test7-module'/)) + // await expectFileToMatch(modulePath, /imports: \[(.|\s)*Test7Module(.|\s)*\]/m)); +} diff --git a/tests/e2e/tests/generate/module/module-routing-child-folder.ts b/tests/e2e/tests/generate/module/module-routing-child-folder.ts new file mode 100644 index 000000000000..f2ba0e3396f7 --- /dev/null +++ b/tests/e2e/tests/generate/module/module-routing-child-folder.ts @@ -0,0 +1,24 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; +import { expectToFail } from '../../../utils/utils'; +import { useCIChrome, useCIDefaults } from '../../../utils/project'; + +export default async function () { + const projectName = 'test-project-two'; + await ng('generate', 'application', projectName, '--no-standalone', '--skip-install'); + await useCIDefaults(projectName); + await useCIChrome(projectName, 'projects/test-project-two'); + + const testPath = join(process.cwd(), `projects/${projectName}/src/app`); + process.chdir(testPath); + + await ng('generate', 'module', 'sub-dir/child', '--routing'); + await expectFileToExist(join(testPath, 'sub-dir/child')); + await expectFileToExist(join(testPath, 'sub-dir/child', 'child-module.ts')); + await expectFileToExist(join(testPath, 'sub-dir/child', 'child-routing-module.ts')); + await expectToFail(() => expectFileToExist(join(testPath, 'sub-dir/child', 'child.spec.ts'))); + + // Try to run the unit tests. + await ng('test', projectName, '--watch=false'); +} diff --git a/tests/e2e/tests/generate/pipe/pipe-basic.ts b/tests/e2e/tests/generate/pipe/pipe-basic.ts new file mode 100644 index 000000000000..2ddb3ff4225f --- /dev/null +++ b/tests/e2e/tests/generate/pipe/pipe-basic.ts @@ -0,0 +1,19 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; + +import { expectFileToExist } from '../../../utils/fs'; + +export default function () { + // Create the pipe in the same directory. + const pipeDir = join('src', 'app'); + + return ( + ng('generate', 'pipe', 'test') + .then(() => expectFileToExist(pipeDir)) + .then(() => expectFileToExist(join(pipeDir, 'test-pipe.ts'))) + .then(() => expectFileToExist(join(pipeDir, 'test-pipe.spec.ts'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/generate/schematic-aliases.ts b/tests/e2e/tests/generate/schematic-aliases.ts new file mode 100644 index 000000000000..926c96d7aacc --- /dev/null +++ b/tests/e2e/tests/generate/schematic-aliases.ts @@ -0,0 +1,14 @@ +import { ng } from '../../utils/process'; + +export default async function () { + const schematicNameVariation = [ + 'component', + 'c', + '@schematics/angular:component', + '@schematics/angular:c', + ]; + + for (const schematic of schematicNameVariation) { + await ng('generate', schematic, 'comp-name', '--display-block', '--dry-run'); + } +} diff --git a/tests/legacy-cli/e2e/tests/generate/schematic-defaults.ts b/tests/e2e/tests/generate/schematic-defaults.ts similarity index 79% rename from tests/legacy-cli/e2e/tests/generate/schematic-defaults.ts rename to tests/e2e/tests/generate/schematic-defaults.ts index 7e015a0d6638..a6fc32a8c52e 100644 --- a/tests/legacy-cli/e2e/tests/generate/schematic-defaults.ts +++ b/tests/e2e/tests/generate/schematic-defaults.ts @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; @@ -12,9 +13,7 @@ export default async function () { // Generate component in application to verify that it's minimal const { stdout } = await ng('generate', 'component', 'foo'); - if (!stdout.includes('foo.component.scss')) { - throw new Error('Expected "foo.component.scss" to exist.'); - } + assert.match(stdout, /foo\.scss/); // Generate another project with different settings await ng('generate', 'application', 'test-project-two', '--no-minimal'); @@ -23,6 +22,7 @@ export default async function () { config.projects['test-project-two'].schematics = { '@schematics/angular:component': { style: 'less', + type: 'Component', }, }; }); @@ -34,7 +34,5 @@ export default async function () { '--project', 'test-project-two', ); - if (!stdout2.includes('foo.component.less')) { - throw new Error('Expected "foo.component.less" to exist.'); - } + assert.match(stdout2, /foo\.component\.less/); } diff --git a/tests/e2e/tests/generate/schematic-force-override.ts b/tests/e2e/tests/generate/schematic-force-override.ts new file mode 100644 index 000000000000..d3e9e1b7d947 --- /dev/null +++ b/tests/e2e/tests/generate/schematic-force-override.ts @@ -0,0 +1,51 @@ +import { appendFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; +import { getActivePackageManager, installWorkspacePackages } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { isPrereleaseCli, updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +const snapshots = require('../../ng-snapshot/package.json'); + +export default async function () { + const isPrerelease = await isPrereleaseCli(); + let tag = isPrerelease ? '@next' : ''; + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); + + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + if (isSnapshotBuild) { + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + // Angular material adds dependencies on other Angular packages + // Iterate over all of the packages to update them to the snapshot version. + for (const [name, version] of Object.entries(snapshots.dependencies)) { + if (name in dependencies) { + dependencies[name] = version; + } + } + }); + await installWorkspacePackages(); + } + + const args: string[] = [ + 'generate', + '@angular/material:theme-color', + '--primary-color=#0641e6', + '--tertiary-color=#994aff', + '--neutral-color=#313138', + '--error-color=#eb5757', + '--secondary-color=#009096', + '--neutral-variant-color=#b2b2b8', + ]; + + await ng(...args); + + // Should fail as file exists + await expectToFail(() => ng(...args)); + + await ng(...args, '--force'); +} diff --git a/tests/e2e/tests/generate/schematics-collections-relative.ts b/tests/e2e/tests/generate/schematics-collections-relative.ts new file mode 100644 index 000000000000..f6f583bf0e72 --- /dev/null +++ b/tests/e2e/tests/generate/schematics-collections-relative.ts @@ -0,0 +1,53 @@ +import assert from 'node:assert'; +import { join } from 'node:path'; +import { ng } from '../../utils/process'; +import { writeMultipleFiles, createDir } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + // setup temp collection + await createDir('./fake-schematics'); + await writeMultipleFiles({ + './fake-schematics/package.json': JSON.stringify({ + 'schematics': './collection.json', + }), + './fake-schematics/collection.json': JSON.stringify({ + 'schematics': { + 'fake': { + 'description': 'Fake schematic', + 'schema': './fake-schema.json', + 'factory': './fake', + }, + }, + }), + './fake-schematics/fake-schema.json': JSON.stringify({ + '$id': 'FakeSchema', + 'title': 'Fake Schema', + 'type': 'object', + }), + './fake-schematics/fake.js': ` + exports.default = () => (host, context) => context.logger.info('fake schematic run.'); + `, + }); + + await updateJsonFile('angular.json', (json) => { + json.cli ??= {}; + json.cli.schematicCollections = ['./fake-schematics']; + }); + + const { stdout: stdout1 } = await ng('generate', '--help'); + assert.match(stdout1, /Fake schematic/); + + const { stdout: stdout2 } = await ng('generate', 'fake'); + assert.match(stdout2, /fake schematic run/); + + // change cwd to a nested directory to validate the relative schematic is resolved correctly + const originalCwd = process.cwd(); + try { + process.chdir(join(originalCwd, 'src/app')); + const { stdout: stdout3 } = await ng('generate', 'fake'); + assert.match(stdout3, /fake schematic run/); + } finally { + process.chdir(originalCwd); + } +} diff --git a/tests/e2e/tests/generate/schematics-collections.ts b/tests/e2e/tests/generate/schematics-collections.ts new file mode 100644 index 000000000000..6c009bf91e5d --- /dev/null +++ b/tests/e2e/tests/generate/schematics-collections.ts @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { createDir, expectFileToExist, writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + // setup temp collection + const genRoot = join('node_modules/fake-schematics/'); + const fakeComponentSchematicDesc = 'Fake component schematic'; + + await createDir(genRoot); + await writeMultipleFiles({ + [join(genRoot, 'package.json')]: JSON.stringify({ + 'schematics': './collection.json', + }), + [join(genRoot, 'collection.json')]: JSON.stringify({ + 'schematics': { + 'fake': { + 'description': 'Fake schematic', + 'schema': './fake-schema.json', + 'factory': './fake', + }, + 'component': { + 'description': fakeComponentSchematicDesc, + 'schema': './fake-schema.json', + 'factory': './fake-component', + }, + }, + }), + [join(genRoot, 'fake-schema.json')]: JSON.stringify({ + '$id': 'FakeSchema', + 'title': 'Fake Schema', + 'type': 'object', + }), + [join(genRoot, 'fake.js')]: ` + exports.default = function (options) { + return (host, context) => { + console.log('fake schematic run.'); + }; + } + `, + [join(genRoot, 'fake-component.js')]: ` + exports.default = function (options) { + return (host, context) => { + console.log('fake component schematic run.'); + }; + } + `, + }); + + await updateJsonFile('angular.json', (json) => { + json.cli ??= {}; + json.cli.schematicCollections = ['fake-schematics', '@schematics/angular']; + }); + + // should display schematics for all schematics + const { stdout: stdout1 } = await ng('generate', '--help'); + assert.match(stdout1, /ng generate component/); + assert.match(stdout1, /ng generate fake/); + + // check registration order. Both schematics contain a component schematic verify that the first one wins. + assert.match(stdout1, new RegExp(fakeComponentSchematicDesc)); + + // Verify execution based on ordering + const { stdout: stdout2 } = await ng('generate', 'component'); + assert.match(stdout2, /fake component schematic run/); + + await updateJsonFile('angular.json', (json) => { + json.cli ??= {}; + json.cli.schematicCollections = ['@schematics/angular', 'fake-schematics']; + }); + + const { stdout: stdout3 } = await ng('generate', '--help'); + assert.match(stdout3, /ng generate component \[name\]/); + assert.doesNotMatch(stdout3, new RegExp(fakeComponentSchematicDesc)); + + // Verify execution based on ordering + const projectDir = join('src', 'app'); + const componentDir = join(projectDir, 'test-component'); + await ng('generate', 'component', 'test-component'); + await expectFileToExist(componentDir); +} diff --git a/tests/e2e/tests/generate/service/service-basic.ts b/tests/e2e/tests/generate/service/service-basic.ts new file mode 100644 index 000000000000..a7ddb3cb8310 --- /dev/null +++ b/tests/e2e/tests/generate/service/service-basic.ts @@ -0,0 +1,18 @@ +import { join } from 'node:path'; +import { ng } from '../../../utils/process'; +import { expectFileToExist } from '../../../utils/fs'; + +export default function () { + // Does not create a sub directory. + const serviceDir = join('src', 'app'); + + return ( + ng('generate', 'service', 'test-service') + .then(() => expectFileToExist(serviceDir)) + .then(() => expectFileToExist(join(serviceDir, 'test-service.ts'))) + .then(() => expectFileToExist(join(serviceDir, 'test-service.spec.ts'))) + + // Try to run the unit tests. + .then(() => ng('test', '--watch=false')) + ); +} diff --git a/tests/e2e/tests/i18n/extract-ivy-disk-cache.ts b/tests/e2e/tests/i18n/extract-ivy-disk-cache.ts new file mode 100644 index 000000000000..562a481086d5 --- /dev/null +++ b/tests/e2e/tests/i18n/extract-ivy-disk-cache.ts @@ -0,0 +1,51 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch, rimraf, writeFile } from '../../utils/fs'; +import { installPackage, uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { readNgVersion } from '../../utils/version'; + +export default async function () { + // Enable disk cache + await updateJsonFile('angular.json', (config) => { + config.cli ??= {}; + config.cli.cache = { environment: 'all' }; + }); + + // Setup an i18n enabled component + await ng('generate', 'component', 'i18n-test'); + await writeFile(join('src/app/i18n-test', 'i18n-test.html'), '

Hello world

'); + + await writeFile( + 'src/app/app.ts', + ` + import { Component } from '@angular/core'; + import { I18nTest } from './i18n-test/i18n-test'; + + @Component({ + selector: 'app-root', + imports: [I18nTest], + template: '' + }) + export class App {} + `, + ); + + // Install correct version + let localizeVersion = '@angular/localize@' + readNgVersion(); + if (getGlobalVariable('argv')['ng-snapshots']) { + localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; + } + + await installPackage(localizeVersion); + + for (let i = 0; i < 2; i++) { + // Run the extraction twice and make sure the second time round works with cache. + await rimraf('messages.xlf'); + await ng('extract-i18n'); + await expectFileToMatch('messages.xlf', 'Hello world'); + } + + await uninstallPackage('@angular/localize'); +} diff --git a/tests/e2e/tests/i18n/extract-ivy-libraries.ts b/tests/e2e/tests/i18n/extract-ivy-libraries.ts new file mode 100644 index 000000000000..a577a8c5e6a6 --- /dev/null +++ b/tests/e2e/tests/i18n/extract-ivy-libraries.ts @@ -0,0 +1,42 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch, prependToFile, replaceInFile, writeFile } from '../../utils/fs'; +import { installPackage, uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { readNgVersion } from '../../utils/version'; + +export default async function () { + // Setup a library + await ng('generate', 'library', 'i18n-lib-test'); + await replaceInFile('projects/i18n-lib-test/src/lib/i18n-lib-test.ts', '

', '

'); + + // Build library + await ng('build', 'i18n-lib-test', '--configuration=development'); + + // Consume library in application + await replaceInFile('src/app/app.ts', 'imports: [', 'imports: [I18nLibTest,'); + await prependToFile('src/app/app.ts', `import { I18nLibTest } from 'i18n-lib-test';`); + + await writeFile( + 'src/app/app.html', + ` +

Hello world

+ + `, + ); + + // Install correct version + let localizeVersion = '@angular/localize@' + readNgVersion(); + if (getGlobalVariable('argv')['ng-snapshots']) { + localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; + } + await installPackage(localizeVersion); + + // Extract messages + await ng('extract-i18n'); + await expectFileToMatch('messages.xlf', 'Hello world'); + await expectFileToMatch('messages.xlf', 'i18n-lib-test works!'); + await expectFileToMatch('messages.xlf', 'src/app/app.html'); + await expectFileToMatch('messages.xlf', 'projects/i18n-lib-test/src/lib/i18n-lib-test.ts'); + + await uninstallPackage('@angular/localize'); +} diff --git a/tests/e2e/tests/i18n/extract-ivy.ts b/tests/e2e/tests/i18n/extract-ivy.ts new file mode 100644 index 000000000000..382051e83cce --- /dev/null +++ b/tests/e2e/tests/i18n/extract-ivy.ts @@ -0,0 +1,56 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch, writeFile } from '../../utils/fs'; +import { uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; +import { readNgVersion } from '../../utils/version'; + +export default async function () { + // Setup an i18n enabled component + await ng('generate', 'component', 'i18n-test'); + await writeFile(join('src/app/i18n-test', 'i18n-test.html'), '

Hello world

'); + // Actually use the generated component to ensure it is present in the application output + await writeFile( + 'src/app/app.ts', + ` + import { Component } from '@angular/core'; + import { I18nTest } from './i18n-test/i18n-test'; + + @Component({ + selector: 'app-root', + imports: [I18nTest], + template: '' + }) + export class App {} + `, + ); + + // Ensure localize package is not present initially + await uninstallPackage('@angular/localize'); + + // Should fail if `@angular/localize` is missing + const { message: message1 } = await expectToFail(() => ng('extract-i18n')); + if (!message1.includes(`i18n extraction requires the '@angular/localize' package.`)) { + throw new Error('Expected localize package error message when missing'); + } + + // Install correct version + let localizeVersion = '@angular/localize@' + readNgVersion(); + if (getGlobalVariable('argv')['ng-snapshots']) { + // The snapshots job won't work correctly because 'ng add' doesn't support github URLs + // localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; + return; + } + await ng('add', localizeVersion, '--skip-confirmation'); + + // Should not show any warnings when extracting + const { stderr: message5 } = await ng('extract-i18n'); + if (message5.includes('WARNING')) { + throw new Error('Expected no warnings to be shown. STDERR:\n' + message5); + } + + await expectFileToMatch('messages.xlf', 'Hello world'); + + await uninstallPackage('@angular/localize'); +} diff --git a/tests/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts b/tests/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts new file mode 100644 index 000000000000..b1568be95939 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts @@ -0,0 +1,85 @@ +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, createDir, expectFileToMatch, writeFile } from '../../utils/fs'; +import { installWorkspacePackages } from '../../utils/packages'; +import { ng, silentNg } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { readNgVersion } from '../../utils/version'; + +const snapshots = require('../../ng-snapshot/package.json'); + +export default async function () { + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/localize'] = isSnapshotBuild + ? snapshots.dependencies['@angular/localize'] + : readNgVersion(); + }); + + await appendToFile('src/app/app.html', ''); + + // Add app-shell and service-worker + await silentNg('generate', 'app-shell'); + await silentNg('generate', 'service-worker'); + + if (isSnapshotBuild) { + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/platform-server'] = snapshots.dependencies['@angular/platform-server']; + dependencies['@angular/service-worker'] = snapshots.dependencies['@angular/service-worker']; + dependencies['@angular/router'] = snapshots.dependencies['@angular/router']; + }); + } + + await installWorkspacePackages(); + + // Set configurations for each locale. + const langTranslations = [ + { lang: 'en-US', translation: 'Hello i18n!' }, + { lang: 'fr', translation: 'Bonjour i18n!' }, + ]; + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const appArchitect = appProject.architect; + const buildOptions = appArchitect['build'].options; + + // Enable localization for all locales + buildOptions.localize = true; + + // Add locale definitions to the project + const i18n: Record = (appProject.i18n = { locales: {} }); + for (const { lang } of langTranslations) { + if (lang == 'en-US') { + i18n.sourceLocale = lang; + } else { + i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; + } + } + }); + + await createDir('src/locale'); + + for (const { lang } of langTranslations) { + // dummy translation file. + await writeFile( + `src/locale/messages.${lang}.xlf`, + ` + + + + `, + ); + } + + // Build each locale and verify the SW output. + await ng('build', '--output-hashing=none'); + + for (const { lang } of langTranslations) { + await Promise.all([ + expectFileToMatch(`dist/test-project/browser/${lang}/ngsw.json`, `/${lang}/main.js`), + expectFileToMatch(`dist/test-project/browser/${lang}/ngsw.json`, `/${lang}/index.html`), + ]); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-app-shell.ts b/tests/e2e/tests/i18n/ivy-localize-app-shell.ts new file mode 100644 index 000000000000..cbabbae17140 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-app-shell.ts @@ -0,0 +1,108 @@ +import { getGlobalVariable } from '../../utils/env'; +import { + appendToFile, + copyFile, + expectFileToMatch, + replaceInFile, + writeFile, +} from '../../utils/fs'; +import { installWorkspacePackages } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { readNgVersion } from '../../utils/version'; + +const snapshots = require('../../ng-snapshot/package.json'); + +export default async function () { + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/localize'] = isSnapshotBuild + ? snapshots.dependencies['@angular/localize'] + : readNgVersion(); + }); + + await appendToFile('src/app/app.html', ''); + await ng('generate', 'app-shell', '--project', 'test-project'); + + if (isSnapshotBuild) { + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/platform-server'] = snapshots.dependencies['@angular/platform-server']; + dependencies['@angular/router'] = snapshots.dependencies['@angular/router']; + }); + } + + await installWorkspacePackages(); + + // Set configurations for each locale. + const langTranslations = [ + { lang: 'en-US', translation: 'Hello i18n!' }, + { lang: 'fr', translation: 'Bonjour i18n!' }, + ]; + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const appArchitect = appProject.architect || appProject.targets; + const buildOptions = appArchitect['build'].options; + + // Enable localization for all locales + buildOptions.localize = true; + + // Add locale definitions to the project + const i18n: Record = (appProject.i18n = { locales: {} }); + for (const { lang } of langTranslations) { + if (lang == 'en-US') { + i18n.sourceLocale = lang; + } else { + i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; + } + } + }); + + await writeFile( + 'src/app/app-shell/app-shell.html', + '

Hello i18n!

', + ); + + // Add a translatable element + // Extraction of i18n only works on browser targets. + // Let's add the same translation that there is in the app-shell + await writeFile( + 'src/app/app.html', + '

Hello i18n!

', + ); + + // Extract the translation messages and copy them for each language. + await ng('extract-i18n', '--output-path=src/locale'); + await expectFileToMatch('src/locale/messages.xlf', `source-language="en-US"`); + await expectFileToMatch('src/locale/messages.xlf', `An introduction header for this sample`); + + // Clean up app.ng.html so that we can easily + // find the translation text + await writeFile('src/app/app.html', ''); + + for (const { lang, translation } of langTranslations) { + if (lang != 'en-US') { + await copyFile('src/locale/messages.xlf', `src/locale/messages.${lang}.xlf`); + await replaceInFile( + `src/locale/messages.${lang}.xlf`, + 'source-language="en-US"', + `source-language="en-US" target-language="${lang}"`, + ); + await replaceInFile( + `src/locale/messages.${lang}.xlf`, + 'Hello i18n!', + `Hello i18n!\n${translation}`, + ); + } + } + + // Build each locale and verify the output. + await ng('build', '--output-mode=static'); + + for (const { lang, translation } of langTranslations) { + await expectFileToMatch(`dist/test-project/browser/${lang}/index.html`, translation); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-basehref-absolute.ts b/tests/e2e/tests/i18n/ivy-localize-basehref-absolute.ts new file mode 100644 index 000000000000..db59853ff759 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-basehref-absolute.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { langTranslations, setupI18nConfig } from './setup'; + +const baseHrefs: { [l: string]: string } = { + 'en-US': '/en/', + fr: '/fr-FR/', + de: '', +}; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = { + baseHref: baseHrefs['en-US'], + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + baseHref: baseHrefs['fr'], + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + baseHref: baseHrefs['de'], + }; + }); + + // Test absolute base href. + await ng('build', '--base-href', 'http://www.example.com/', '--configuration=development'); + for (const { lang, outputPath } of langTranslations) { + // Verify the HTML base HREF attribute is present + await expectFileToMatch( + `${outputPath}/index.html`, + `href="http://www.example.com${baseHrefs[lang] || '/'}"`, + ); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-basehref.ts b/tests/e2e/tests/i18n/ivy-localize-basehref.ts new file mode 100644 index 000000000000..4bbd7dece3ee --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-basehref.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; +import { + baseHrefs, + browserCheck, + externalServer, + langTranslations, + setupI18nConfig, +} from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = { + baseHref: baseHrefs['en-US'], + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + baseHref: baseHrefs['fr'], + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + baseHref: baseHrefs['de'], + }; + }); + + // Build each locale and verify the output. + await ng('build'); + for (const { lang, outputPath } of langTranslations) { + if (baseHrefs[lang] === undefined) { + throw new Error('Invalid E2E test setup: unexpected locale ' + lang); + } + + // Verify the HTML lang attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); + + // Verify the HTML base HREF attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `href="${baseHrefs[lang] || '/'}"`); + + // Execute Application E2E tests for a production build without dev server + const { server, url } = await externalServer(outputPath, (baseHrefs[lang] as string) || '/'); + try { + await executeBrowserTest({ + baseUrl: url, + checkFn: (page) => browserCheck(page, lang), + }); + } finally { + server.close(); + } + } + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + + appArchitect['build'].options.baseHref = '/test/'; + }); + + // Build each locale and verify the output. + await ng('build', '--configuration=development'); + for (const { lang, outputPath } of langTranslations) { + // Verify the HTML base HREF attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `href="/test${baseHrefs[lang] || '/'}"`); + + // Execute Application E2E tests for a production build without dev server + const { server, url } = await externalServer(outputPath, '/test' + (baseHrefs[lang] || '/')); + try { + await executeBrowserTest({ + baseUrl: url, + checkFn: (page) => browserCheck(page, lang), + }); + } finally { + server.close(); + } + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-es2015-e2e.ts b/tests/e2e/tests/i18n/ivy-localize-es2015-e2e.ts new file mode 100644 index 000000000000..d1f7ac8e28b8 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-es2015-e2e.ts @@ -0,0 +1,14 @@ +import { executeBrowserTest } from '../../utils/puppeteer'; +import { browserCheck, langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + for (const { lang } of langTranslations) { + await executeBrowserTest({ + configuration: lang, + checkFn: (page) => browserCheck(page, lang), + }); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/e2e/tests/i18n/ivy-localize-es2015.ts new file mode 100644 index 000000000000..cea87a75f2b8 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-es2015.ts @@ -0,0 +1,46 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { executeBrowserTest } from '../../utils/puppeteer'; +import { expectToFail } from '../../utils/utils'; +import { browserCheck, externalServer, langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + const { stderr } = await ng('build'); + if (/Locale data for .+ cannot be found/.test(stderr)) { + throw new Error( + `A warning for a locale not found was shown. This should not happen.\n\nSTDERR:\n${stderr}\n`, + ); + } + + for (const { lang, outputPath, translation } of langTranslations) { + await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial); + await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`')); + + // Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references) + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // The only reference in a new application with Webpack is in @angular/core + await expectFileToMatch(`${outputPath}/vendor.js`, lang); + } else { + await expectFileToMatch(`${outputPath}/polyfills.js`, lang); + } + + // Verify the HTML lang attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); + + // Execute Application E2E tests for a production build without dev server + const { server, url } = await externalServer(outputPath, `/${lang}/`); + try { + await executeBrowserTest({ + baseUrl: url, + checkFn: (page) => browserCheck(page, lang), + }); + } finally { + server.close(); + } + } +} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-hashes.ts b/tests/e2e/tests/i18n/ivy-localize-hashes.ts similarity index 85% rename from tests/legacy-cli/e2e/tests/i18n/ivy-localize-hashes.ts rename to tests/e2e/tests/i18n/ivy-localize-hashes.ts index df60a124034d..2f682ca76988 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-hashes.ts +++ b/tests/e2e/tests/i18n/ivy-localize-hashes.ts @@ -3,17 +3,17 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { appendToFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { langTranslations, setupI18nConfig } from './setup'; -const OUTPUT_RE = /^(?(?:main|vendor|\d+))\.(?[a-z0-9]+)\.js$/i; +const OUTPUT_RE = /^(?(?:main|vendor|\d+))(?:\.|-)(?[a-z0-9]+)\.js$/i; -export default async function() { +export default async function () { // Setup i18n tests and config. await setupI18nConfig(); @@ -23,7 +23,7 @@ export default async function() { for (const { lang, outputPath } of langTranslations) { for (const entry of fs.readdirSync(outputPath)) { const match = entry.match(OUTPUT_RE); - if (!match) { + if (!match?.groups) { continue; } @@ -44,7 +44,7 @@ export default async function() { for (const { lang, outputPath } of langTranslations) { for (const entry of fs.readdirSync(outputPath)) { const match = entry.match(OUTPUT_RE); - if (!match) { + if (!match?.groups) { continue; } @@ -53,7 +53,7 @@ export default async function() { if (!hash) { throw new Error('Unexpected output entry: ' + id); } - if (hash === match.groups.hash) { + if (hash === match.groups!.hash) { throw new Error('Hash value did not change for entry: ' + id); } diff --git a/tests/e2e/tests/i18n/ivy-localize-locale-data-augment.ts b/tests/e2e/tests/i18n/ivy-localize-locale-data-augment.ts new file mode 100644 index 000000000000..b4c0ae72ced4 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-locale-data-augment.ts @@ -0,0 +1,63 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch, prependToFile, readFile, writeFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; +import { browserCheck, langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json to only localize one locale + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + appProject.architect['build'].options.localize = ['fr']; + }); + + // Augment the locale data and import into the main application file + const localeData = await readFile('node_modules/@angular/common/locales/global/fr.js'); + await writeFile('src/fr-changed.js', localeData.replace('janvier', 'changed-janvier')); + await prependToFile('src/main.ts', "import './fr-changed.js';\n"); + + // Run a build and test + await ng('build'); + for (const { lang, outputPath } of langTranslations) { + // Only the fr locale was built for this test + if (lang !== 'fr') { + continue; + } + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // The only reference in a new application with Webpack is in @angular/core + await expectFileToMatch(`${outputPath}/vendor.js`, lang); + } else { + await expectFileToMatch(`${outputPath}/polyfills.js`, lang); + } + + // Execute Application E2E tests with dev server + await executeBrowserTest({ + configuration: lang, + checkFn: async (page) => { + // Run standard checks but expect failure on date + try { + await browserCheck(page, lang); + throw new Error('Expected browserCheck to fail due to modified locale data'); + } catch (e) { + if (!(e instanceof Error) || !e.message.includes("Expected 'date' to be")) { + throw e; + } + } + + // Verify the modified date + const getParagraph = async (id: string) => + page.$eval(`p#${id}`, (el) => el.textContent?.trim()); + const date = await getParagraph('date'); + if (date !== 'changed-janvier') { + throw new Error(`Expected 'date' to be 'changed-janvier', but got '${date}'.`); + } + }, + }); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-locale-data.ts b/tests/e2e/tests/i18n/ivy-localize-locale-data.ts new file mode 100644 index 000000000000..d378ba5c56a9 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-locale-data.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = 'fr-Abcd'; + appProject.architect['build'].options.localize = ['fr-Abcd']; + }); + + const { stderr: err1 } = await ng('build'); + if (!err1.includes(`Locale data for 'fr-Abcd' cannot be found. Using locale data for 'fr'.`)) { + throw new Error('locale data fallback warning not shown'); + } + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = 'en-US'; + appProject.architect['build'].options.localize = ['en-US']; + }); + + const { stderr: err2 } = await ng('build'); + if ( + err2.includes( + `Locale data for 'en-US' cannot be found. No locale data will be included for this locale.`, + ) + ) { + throw new Error('locale data not found warning shown'); + } + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = 'en-x-abc'; + appProject.architect['build'].options.localize = ['en-x-abc']; + }); + + const { stderr: err3 } = await ng('build', '--configuration=development'); + if ( + err3.includes( + `Locale data for 'en-x-abc' cannot be found. No locale data will be included for this locale.`, + ) + ) { + throw new Error('locale data not found warning shown'); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-merging.ts b/tests/e2e/tests/i18n/ivy-localize-merging.ts new file mode 100644 index 000000000000..f4e016951094 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-merging.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 { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; +import { setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const i18n: Record = appProject.i18n; + + i18n.locales['fr'] = [i18n.locales['fr'], i18n.locales['fr']]; + appProject.architect['build'].options.localize = ['fr']; + }); + + const { stderr: err1 } = await ng('build'); + if (!err1.includes('Duplicate translations for message')) { + throw new Error('duplicate translations warning not shown'); + } + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + appProject.architect['build'].options.i18nDuplicateTranslation = 'error'; + }); + await expectToFail(() => ng('build')); + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + appProject.architect['build'].options.i18nDuplicateTranslation = 'ignore'; + }); + const { stderr: err2 } = await ng('build'); + if (err2.includes('Duplicate translations for message')) { + throw new Error('duplicate translations message not ignore'); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-sourcelocale.ts b/tests/e2e/tests/i18n/ivy-localize-sourcelocale.ts new file mode 100644 index 000000000000..e88fc571e289 --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-sourcelocale.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = 'fr'; + + delete i18n.locales['fr']; + }); + + // Build each locale and verify the output. + await ng('build', '--configuration=development'); + for (const { lang, outputPath } of langTranslations) { + // does not exist in this test due to the source locale change + if (lang === 'en-US') { + continue; + } + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // The only reference in a new application with Webpack is in @angular/core + await expectFileToMatch(`${outputPath}/vendor.js`, lang); + + // Verify the locale data is registered using the global files + await expectFileToMatch(`${outputPath}/vendor.js`, '.ng.common.locales'); + } else { + await expectFileToMatch(`${outputPath}/polyfills.js`, lang); + + // Verify the locale data is registered using the global files + await expectFileToMatch(`${outputPath}/polyfills.js`, '.ng.common.locales'); + } + + // Verify the HTML lang attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-sourcemaps.ts b/tests/e2e/tests/i18n/ivy-localize-sourcemaps.ts new file mode 100644 index 000000000000..8f65ef450c0e --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-sourcemaps.ts @@ -0,0 +1,22 @@ +import { readFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + await ng('build', '--source-map'); + + for (const { outputPath } of langTranslations) { + // Ensure sourcemap for modified file contains content + const mainSourceMap = JSON.parse(await readFile(`${outputPath}/main.js.map`)); + if ( + mainSourceMap.version !== 3 || + !Array.isArray(mainSourceMap.sources) || + typeof mainSourceMap.mappings !== 'string' + ) { + throw new Error('invalid localized sourcemap for main.js'); + } + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-ssr.ts b/tests/e2e/tests/i18n/ivy-localize-ssr.ts new file mode 100644 index 000000000000..dd0c75ae74fc --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-ssr.ts @@ -0,0 +1,80 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { getGlobalVariable } from '../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { updateJsonFile, useSha } from '../../utils/project'; +import { langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + // This test is for the `application` builder only + return; + } + + // Setup i18n tests and config. + await setupI18nConfig(); + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + // tslint:disable-next-line: no-any + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = { + baseHref: '', + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + baseHref: '', + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + baseHref: '', + }; + }); + + // forcibly remove in case another test doesn't clean itself up + await uninstallPackage('@angular/ssr'); + + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + + await useSha(); + await installWorkspacePackages(); + + // Build each locale and verify the output. + await ng('build', '--output-hashing=none'); + + for (const { lang, translation } of langTranslations) { + let foundTranslation = false; + let foundLocaleData = false; + + // The translation may be in any of the lazy-loaded generated chunks + for (const entry of readdirSync(`dist/test-project/server/${lang}/`)) { + if (!entry.endsWith('.mjs')) { + continue; + } + + const contents = readFileSync(`dist/test-project/server/${lang}/${entry}`, 'utf-8'); + + // Check for translated content + foundTranslation ||= contents.includes(translation.helloPartial); + // Check for the locale data month name to be present + foundLocaleData ||= contents.includes(translation.date); + + if (foundTranslation && foundLocaleData) { + break; + } + } + + if (!foundTranslation) { + throw new Error(`Translation not found in 'dist/test-project/server/${lang}/'`); + } + + if (!foundLocaleData) { + throw new Error(`Locale data not found in 'dist/test-project/server/${lang}/'`); + } + } +} diff --git a/tests/e2e/tests/i18n/ivy-localize-sub-path.ts b/tests/e2e/tests/i18n/ivy-localize-sub-path.ts new file mode 100644 index 000000000000..6116c438a49f --- /dev/null +++ b/tests/e2e/tests/i18n/ivy-localize-sub-path.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 { join } from 'node:path'; +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; +import { baseDir, browserCheck, externalServer, langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + const URL_SUB_PATH: Record = { + 'en-US': '', + 'fr': 'fr', + 'de': 'deutsche', + }; + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = { + subPath: URL_SUB_PATH['en-US'], + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + subPath: URL_SUB_PATH['fr'], + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + subPath: URL_SUB_PATH['de'], + }; + }); + + // Build each locale and verify the output. + await ng('build'); + for (const { lang } of langTranslations) { + const subPath = URL_SUB_PATH[lang]; + const baseHref = subPath ? `/${subPath}/` : '/'; + const outputPath = join(baseDir, subPath); + + // Verify the HTML lang attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); + + // Verify the HTML base HREF attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `href="${baseHref}"`); + + // Execute Application E2E tests for a production build without dev server + const { server, url } = await externalServer(outputPath, baseHref); + + try { + await executeBrowserTest({ + baseUrl: url, + checkFn: (page) => browserCheck(page, lang), + }); + } finally { + server.close(); + } + } +} diff --git a/tests/e2e/tests/i18n/setup.ts b/tests/e2e/tests/i18n/setup.ts new file mode 100644 index 000000000000..8eade4e2783c --- /dev/null +++ b/tests/e2e/tests/i18n/setup.ts @@ -0,0 +1,275 @@ +import express from 'express'; +import { dirname, resolve } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, copyFile, createDir, replaceInFile, writeFile } from '../../utils/fs'; +import { installPackage } from '../../utils/packages'; +import { updateJsonFile } from '../../utils/project'; +import { readNgVersion } from '../../utils/version'; +import { Server } from 'node:http'; +import { AddressInfo } from 'node:net'; +import type { Page } from 'puppeteer'; + +// Configurations for each locale. +const translationFile = 'src/locale/messages.xlf'; +export const baseDir = 'dist/test-project/browser'; +export const langTranslations = [ + { + lang: 'en-US', + outputPath: `${baseDir}/en-US`, + translation: { + helloPartial: 'Hello', + hello: 'Hello i18n!', + plural: 'Updated 3 minutes ago', + date: 'January', + }, + }, + { + lang: 'fr', + outputPath: `${baseDir}/fr`, + translation: { + helloPartial: 'Bonjour', + hello: 'Bonjour i18n!', + plural: 'Mis à jour il y a 3 minutes', + date: 'janvier', + }, + translationReplacements: [ + ['Hello', 'Bonjour'], + ['Updated', 'Mis à jour'], + ['just now', 'juste maintenant'], + ['one minute ago', 'il y a une minute'], + [/other {/g, 'other {il y a '], + ['minutes ago', 'minutes'], + ], + }, + { + lang: 'de', + outputPath: `${baseDir}/de`, + translation: { + helloPartial: 'Hallo', + hello: 'Hallo i18n!', + plural: 'Aktualisiert vor 3 Minuten', + date: 'Januar', + }, + translationReplacements: [ + ['Hello', 'Hallo'], + ['Updated', 'Aktualisiert'], + ['just now', 'gerade jetzt'], + ['one minute ago', 'vor einer Minute'], + [/other {/g, 'other {vor '], + ['minutes ago', 'Minuten'], + ], + }, +]; +export const sourceLocale = langTranslations[0].lang; + +export async function browserCheck(page: Page, lang: string) { + const translation = langTranslations.find((t) => t.lang === lang)?.translation; + if (!translation) { + throw new Error(`Could not find translation for language '${lang}'`); + } + + const getParagraph = async (id: string) => page.$eval(`p#${id}`, (el) => el.textContent?.trim()); + + const hello = await getParagraph('hello'); + if (hello !== translation.hello) { + throw new Error(`Expected 'hello' to be '${translation.hello}', but got '${hello}'.`); + } + + const locale = await getParagraph('locale'); + if (locale !== lang) { + throw new Error(`Expected 'locale' to be '${lang}', but got '${locale}'.`); + } + + const date = await getParagraph('date'); + if (date !== translation.date) { + throw new Error(`Expected 'date' to be '${translation.date}', but got '${date}'.`); + } + + const plural = await getParagraph('plural'); + if (plural !== translation.plural) { + throw new Error(`Expected 'plural' to be '${translation.plural}', but got '${plural}'.`); + } +} + +export interface ExternalServer { + readonly server: Server; + readonly port: number; + readonly url: string; +} + +/** + * Create an `express` `http.Server` listening on a random port. + * + * Call .close() on the server return value to close the server. + */ +export async function externalServer(outputPath: string, baseUrl = '/'): Promise { + const app = express(); + app.use(baseUrl, express.static(resolve(outputPath))); + + return new Promise((resolve) => { + const server = app.listen(0, 'localhost', () => { + const { port } = server.address() as AddressInfo; + + resolve({ + server, + port, + url: `http://localhost:${port}${baseUrl}`, + }); + }); + }); +} + +export const baseHrefs: { [l: string]: string } = { + 'en-US': '/en/', + fr: '/fr-FR/', + de: '', +}; + +export async function setupI18nConfig() { + // Add component with i18n content, both translations and localeData (plural, dates). + await writeFile( + 'src/app/app.ts', + ` + import { Component, Inject, LOCALE_ID } from '@angular/core'; + import { DatePipe } from '@angular/common'; + import { RouterOutlet } from '@angular/router'; + + @Component({ + selector: 'app-root', + imports: [DatePipe, RouterOutlet], + templateUrl: './app.html' + }) + export class App { + constructor(@Inject(LOCALE_ID) public locale: string) { } + title = 'i18n'; + jan = new Date(2000, 0, 1); + minutes = 3; + } + `, + ); + await writeFile( + `src/app/app.html`, + ` +

Hello {{ title }}!

+

{{ locale }}

+

{{ jan | date : 'LLLL' }}

+

Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}

+ + `, + ); + + await createDir(dirname(translationFile)); + await writeFile( + translationFile, + ` + + + + + + Hello ! + + src/app/app.html + 2,3 + + An introduction header for this sample + + + Updated + + src/app/app.html + 5,6 + + + + {VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other { minutes ago}} + + src/app/app.html + 5,6 + + + + + `, + ); + + // Add a dynamic import to ensure syntax is supported + // ng serve support: https://github.com/angular/angular-cli/issues/16248 + await writeFile('src/app/dynamic.ts', `export const abc = 5;`); + await appendToFile( + 'src/app/app.ts', + ` + (async () => { await import('./dynamic'); })(); + `, + ); + + // Update angular.json to build, serve, and test each locale. + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const appArchitect = workspaceJson.projects['test-project'].architect; + const buildConfigs = appArchitect['build'].configurations; + const serveConfigs = appArchitect['serve'].configurations; + + appArchitect['build'].defaultConfiguration = undefined; + + // Always error on missing translations. + appArchitect['build'].options.optimization = true; + appArchitect['build'].options.aot = true; + appArchitect['build'].options.i18nMissingTranslation = 'error'; + appArchitect['build'].options.sourceMap = true; + appArchitect['build'].options.outputHashing = 'none'; + + const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; + if (useWebpackBuilder) { + appArchitect['build'].options.buildOptimizer = true; + appArchitect['build'].options.vendorChunk = true; + } + + // Enable localization for all locales + appArchitect['build'].options.localize = true; + + // Add i18n config items (app, build, serve, e2e). + // tslint:disable-next-line: no-any + const i18n: Record = (appProject.i18n = { locales: {} }); + for (const { lang } of langTranslations) { + if (lang === sourceLocale) { + i18n.sourceLocale = lang; + } else { + i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; + } + + buildConfigs[lang] = { localize: [lang] }; + serveConfigs[lang] = { buildTarget: `test-project:build:${lang}` }; + } + }); + + // Install the localize package if using ivy + let localizeVersion = '@angular/localize@' + readNgVersion(); + if (getGlobalVariable('argv')['ng-snapshots']) { + localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; + } + + await installPackage(localizeVersion); + + // Make translations for each language. + for (const { lang, translationReplacements } of langTranslations) { + if (lang != sourceLocale) { + await copyFile(translationFile, `src/locale/messages.${lang}.xlf`); + for (const replacements of translationReplacements!) { + await replaceInFile( + `src/locale/messages.${lang}.xlf`, + new RegExp(replacements[0], 'g'), + replacements[1] as string, + ); + } + + for (const replacement of [[/source/g, 'target']]) { + await replaceInFile( + `src/locale/messages.${lang}.xlf`, + new RegExp(replacement[0], 'g'), + replacement[1] as string, + ); + } + } + } +} diff --git a/tests/e2e/tests/jest/aot.ts b/tests/e2e/tests/jest/aot.ts new file mode 100644 index 000000000000..b015e2a58757 --- /dev/null +++ b/tests/e2e/tests/jest/aot.ts @@ -0,0 +1,41 @@ +import { deleteFile, writeFile } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; +import { applyJestBuilder } from '../../utils/jest'; +import { ng } from '../../utils/process'; + +export default async function (): Promise { + await applyJestBuilder(); + + { + await updateJsonFile('tsconfig.spec.json', (json) => { + return { + ...json, + include: ['src/**/*.spec.ts'], + }; + }); + + await writeFile( + 'src/aot.spec.ts', + ` + import { Component } from '@angular/core'; + + describe('Hello', () => { + it('should *not* contain jit instructions', () => { + @Component({ + template: 'Hello', + }) + class Hello {} + + expect((Hello as any).ɵcmp.template.toString()).not.toContain('jit'); + }); + }); + `.trim(), + ); + + const { stderr } = await ng('test', '--aot'); + + if (!stderr.includes('Ran all test suites.') || stderr.includes('failed')) { + throw new Error(`Components were not transformed using AOT.\STDERR:\n\n${stderr}`); + } + } +} diff --git a/tests/e2e/tests/jest/basic.ts b/tests/e2e/tests/jest/basic.ts new file mode 100644 index 000000000000..2a3b19119edd --- /dev/null +++ b/tests/e2e/tests/jest/basic.ts @@ -0,0 +1,12 @@ +import { applyJestBuilder } from '../../utils/jest'; +import { ng } from '../../utils/process'; + +export default async function (): Promise { + await applyJestBuilder(); + + const { stderr } = await ng('test'); + + if (!stderr.includes('Jest builder is currently EXPERIMENTAL')) { + throw new Error(`No experimental notice in stderr.\nSTDERR:\n\n${stderr}`); + } +} diff --git a/tests/e2e/tests/jest/custom-config.ts b/tests/e2e/tests/jest/custom-config.ts new file mode 100644 index 000000000000..10e481a43fcc --- /dev/null +++ b/tests/e2e/tests/jest/custom-config.ts @@ -0,0 +1,53 @@ +import { deleteFile, writeFile } from '../../utils/fs'; +import { applyJestBuilder } from '../../utils/jest'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function (): Promise { + await applyJestBuilder(); + + { + // Users may incorrectly write a Jest config believing it to be used by Angular. + await writeFile( + 'jest.config.mjs', + ` + export default { + runner: 'does-not-exist', + }; + `.trim(), + ); + + // Should not fail from the above (broken) configuration. Shouldn't use it at all. + const { stderr } = await ng('test'); + + // Should warn that a Jest configuration was found but not used. + if (!stderr.includes('A custom Jest config was found')) { + throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`); + } + if (!stderr.includes('jest.config.mjs')) { + throw new Error(`Warning did not call out 'jest.config.mjs':\nSTDERR:\n\n${stderr}`); + } + + await deleteFile('jest.config.mjs'); + } + + { + // Use `package.json` configuration instead of a `jest.config` file. + await updateJsonFile('package.json', (json) => { + json['jest'] = { + runner: 'does-not-exist', + }; + }); + + // Should not fail from the above (broken) configuration. Shouldn't use it at all. + const { stderr } = await ng('test'); + + // Should warn that a Jest configuration was found but not used. + if (!stderr.includes('A custom Jest config was found')) { + throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`); + } + if (!stderr.includes('package.json')) { + throw new Error(`Warning did not call out 'package.json':\nSTDERR:\n\n${stderr}`); + } + } +} diff --git a/tests/e2e/tests/jest/no-zoneless.ts b/tests/e2e/tests/jest/no-zoneless.ts new file mode 100644 index 000000000000..9a74a0295c4e --- /dev/null +++ b/tests/e2e/tests/jest/no-zoneless.ts @@ -0,0 +1,29 @@ +import { replaceInFile } from '../../utils/fs'; +import { applyJestBuilder } from '../../utils/jest'; +import { installPackage, uninstallPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; + +export default async function (): Promise { + await applyJestBuilder({ + tsConfig: 'tsconfig.spec.json', + polyfills: ['zone.js', 'zone.js/testing'], + }); + + await replaceInFile( + 'src/app/app.spec.ts', + 'await fixture.whenStable();', + 'fixture.detectChanges();', + ); + + try { + await installPackage('zone.js'); + + const { stderr } = await ng('test'); + + if (!stderr.includes('Jest builder is currently EXPERIMENTAL')) { + throw new Error(`No experimental notice in stderr.\nSTDERR:\n\n${stderr}`); + } + } finally { + await uninstallPackage('zone.js'); + } +} diff --git a/tests/e2e/tests/mcp/ai-tutor.ts b/tests/e2e/tests/mcp/ai-tutor.ts new file mode 100644 index 000000000000..220b32fcc202 --- /dev/null +++ b/tests/e2e/tests/mcp/ai-tutor.ts @@ -0,0 +1,40 @@ +import { chdir } from 'node:process'; +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + const result = await exec( + MCP_INSPECTOR_COMMAND_NAME, + '--cli', + 'npx', + '--no', + '@angular/cli', + 'mcp', + ...args, + ); + + return result; +} + +export default async function () { + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + // Ensure `get_best_practices` returns the markdown content + const { stdout: stdoutInsideWorkspace } = await runInspector( + '--method', + 'tools/call', + '--tool-name', + 'ai_tutor', + ); + + assert.match(stdoutInsideWorkspace, /# `airules.md` - Modern Angular Tutor 🧑â€ðŸ«/); +} diff --git a/tests/e2e/tests/mcp/best-practices.ts b/tests/e2e/tests/mcp/best-practices.ts new file mode 100644 index 000000000000..55736c63795b --- /dev/null +++ b/tests/e2e/tests/mcp/best-practices.ts @@ -0,0 +1,43 @@ +import { chdir } from 'node:process'; +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + const result = await exec( + MCP_INSPECTOR_COMMAND_NAME, + '--cli', + 'npx', + '--no', + '@angular/cli', + 'mcp', + ...args, + ); + + return result; +} + +export default async function () { + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + // Ensure `get_best_practices` returns the markdown content + const { stdout: stdoutInsideWorkspace } = await runInspector( + '--method', + 'tools/call', + '--tool-name', + 'get_best_practices', + ); + + assert.match( + stdoutInsideWorkspace, + /You are an expert in TypeScript, Angular, and scalable web application development./, + ); +} diff --git a/tests/e2e/tests/mcp/find-examples-basic.ts b/tests/e2e/tests/mcp/find-examples-basic.ts new file mode 100644 index 000000000000..b7f42045076c --- /dev/null +++ b/tests/e2e/tests/mcp/find-examples-basic.ts @@ -0,0 +1,48 @@ +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + const result = await exec( + MCP_INSPECTOR_COMMAND_NAME, + '--cli', + 'npx', + '--no', + '@angular/cli', + 'mcp', + ...args, + ); + + return result; +} + +export default async function () { + const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number); + if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) { + console.log('Test bypassed: find_examples tool requires Node.js 22.16 or higher.'); + + return; + } + + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + // Ensure `get_best_practices` returns the markdown content + const { stdout: stdoutInsideWorkspace } = await runInspector( + '--method', + 'tools/call', + '--tool-name', + 'find_examples', + '--tool-arg', + 'query=if', + ); + + assert.match(stdoutInsideWorkspace, /Using the @if Built-in Control Flow Block/); +} diff --git a/tests/e2e/tests/mcp/registers-tools.ts b/tests/e2e/tests/mcp/registers-tools.ts new file mode 100644 index 000000000000..abc76a99f5d7 --- /dev/null +++ b/tests/e2e/tests/mcp/registers-tools.ts @@ -0,0 +1,49 @@ +import { chdir } from 'node:process'; +import { exec, ProcessOutput, silentNpm } from '../../utils/process'; +import assert from 'node:assert/strict'; + +const MCP_INSPECTOR_PACKAGE_NAME = '@modelcontextprotocol/inspector-cli'; +const MCP_INSPECTOR_PACKAGE_VERSION = '0.16.2'; +const MCP_INSPECTOR_COMMAND_NAME = 'mcp-inspector-cli'; + +async function runInspector(...args: string[]): Promise { + const result = await exec( + MCP_INSPECTOR_COMMAND_NAME, + '--cli', + 'npx', + '--no', + '@angular/cli', + 'mcp', + ...args, + ); + + return result; +} + +export default async function () { + await silentNpm( + 'install', + '--ignore-scripts', + '-g', + `${MCP_INSPECTOR_PACKAGE_NAME}@${MCP_INSPECTOR_PACKAGE_VERSION}`, + ); + + // Ensure 'list_projects' is registered when inside an Angular workspace + try { + const { stdout: stdoutInsideWorkspace } = await runInspector('--method', 'tools/list'); + + assert.match(stdoutInsideWorkspace, /"list_projects"/); + assert.match(stdoutInsideWorkspace, /"get_best_practices"/); + assert.match(stdoutInsideWorkspace, /"search_documentation"/); + + chdir('..'); + + const { stdout: stdoutOutsideWorkspace } = await runInspector('--method', 'tools/list'); + + assert.match(stdoutOutsideWorkspace, /"list_projects"/); + assert.match(stdoutOutsideWorkspace, /"get_best_practices"/); + assert.match(stdoutInsideWorkspace, /"search_documentation"/); + } finally { + await silentNpm('uninstall', '-g', MCP_INSPECTOR_PACKAGE_NAME); + } +} diff --git a/tests/e2e/tests/misc/ask-missing-builder.ts b/tests/e2e/tests/misc/ask-missing-builder.ts new file mode 100644 index 000000000000..ce4270cdbb47 --- /dev/null +++ b/tests/e2e/tests/misc/ask-missing-builder.ts @@ -0,0 +1,24 @@ +import { execAndWaitForOutputToMatch, killAllProcesses } from '../../utils/process'; + +export default async function () { + // Execute a command with TTY force enabled and check that the prompt is shown. + await execAndWaitForOutputToMatch( + 'ng', + ['deploy'], + /Would you like to add a package with "deploy" capabilities/, + { + ...process.env, + NG_FORCE_TTY: '1', + NG_CLI_ANALYTICS: 'false', + }, + ); + + await killAllProcesses(); + + // Execute a command with TTY force enabled and check that the prompt is shown. + await execAndWaitForOutputToMatch('ng', ['lint'], /Would you like to add ESLint now/, { + ...process.env, + NG_FORCE_TTY: '1', + NG_CLI_ANALYTICS: 'false', + }); +} diff --git a/tests/e2e/tests/misc/browsers.ts b/tests/e2e/tests/misc/browsers.ts new file mode 100644 index 000000000000..14c085abaac5 --- /dev/null +++ b/tests/e2e/tests/misc/browsers.ts @@ -0,0 +1,52 @@ +import express from 'express'; +import * as path from 'node:path'; +import { copyProjectAsset } from '../../utils/assets'; +import { replaceInFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + // Ensure SauceLabs configuration + if (!process.env['SAUCE_USERNAME'] || !process.env['SAUCE_ACCESS_KEY']) { + throw new Error('SauceLabs is not configured.'); + } + + // Workaround for https://github.com/angular/angular/issues/32192 + await replaceInFile('src/app/app.html', /class="material-icons"/g, ''); + + await ng('build'); + + // Add Protractor configuration + await copyProjectAsset('protractor-saucelabs.conf.js', 'e2e/protractor-saucelabs.conf.js'); + + // Remove browser log checks as they are only supported with the chrome webdriver + await replaceInFile( + 'e2e/src/app.e2e-spec.ts', + 'await browser.manage().logs().get(logging.Type.BROWSER)', + '[] as any', + ); + + // Workaround defect in getText WebDriver implementation for Safari/Edge + // Leading and trailing space is not removed + await replaceInFile( + 'e2e/src/app.e2e-spec.ts', + 'await page.getTitleText()', + '(await page.getTitleText()).trim()', + ); + + // Setup server + const app = express(); + app.use(express.static(path.resolve('dist/test-project/browser'))); + const server = app.listen(2000, 'localhost'); + + try { + // Execute application's E2E tests with SauceLabs + await ng( + 'e2e', + 'test-project', + '--protractor-config=e2e/protractor-saucelabs.conf.js', + '--dev-server-target=', + ); + } finally { + server.close(); + } +} diff --git a/tests/e2e/tests/misc/check-postinstalls.ts b/tests/e2e/tests/misc/check-postinstalls.ts new file mode 100644 index 000000000000..7c1d63e6ef97 --- /dev/null +++ b/tests/e2e/tests/misc/check-postinstalls.ts @@ -0,0 +1,69 @@ +import glob from 'fast-glob'; +import { readFile } from '../../utils/fs'; + +const CURRENT_SCRIPT_PACKAGES: ReadonlySet = new Set([ + '@parcel/watcher (install)', + 'esbuild (postinstall)', + 'lmdb (install)', + 'msgpackr-extract (install)', + 'nice-napi (install)', + 'unrs-resolver (postinstall)', +]); + +const POTENTIAL_SCRIPTS: ReadonlyArray = ['preinstall', 'install', 'postinstall']; + +// Some packages include test and/or example code that causes false positives +const FALSE_POSITIVE_PATHS: ReadonlySet = new Set([ + 'jasmine-spec-reporter/examples/protractor/package.json', + 'resolve/test/resolver/multirepo/package.json', + 'resolve/test/list-exports/packages/tests/fixtures/resolve-1/project/test/resolver/multirepo/package.json', + 'resolve/test/list-exports/packages/tests/fixtures/resolve-2/project/test/resolver/multirepo/package.json', +]); + +const INNER_NODE_MODULES_SEGMENT = '/node_modules/'; + +export default async function () { + const manifestPaths = await glob('node_modules/**/package.json'); + const newPackages: string[] = []; + + for (const manifestPath of manifestPaths) { + const lastNodeModuleIndex = manifestPath.lastIndexOf(INNER_NODE_MODULES_SEGMENT); + const packageRelativePath = manifestPath.slice( + lastNodeModuleIndex === -1 + ? INNER_NODE_MODULES_SEGMENT.length - 1 + : lastNodeModuleIndex + INNER_NODE_MODULES_SEGMENT.length, + ); + if (FALSE_POSITIVE_PATHS.has(packageRelativePath)) { + continue; + } + + let manifest; + try { + manifest = JSON.parse(await readFile(manifestPath)); + } catch { + continue; + } + + if (!manifest.scripts) { + continue; + } + + for (const script of POTENTIAL_SCRIPTS) { + if (!manifest.scripts[script]) { + continue; + } + + const packageScript = `${manifest.name} (${script})`; + + if (!CURRENT_SCRIPT_PACKAGES.has(packageScript)) { + newPackages.push(packageScript + `[${manifestPath}]`); + } + } + } + + if (newPackages.length) { + throw new Error( + 'New install script package(s) detected:\n' + JSON.stringify(newPackages, null, 2), + ); + } +} diff --git a/tests/e2e/tests/misc/create-angular.ts b/tests/e2e/tests/misc/create-angular.ts new file mode 100644 index 000000000000..acbdf135359b --- /dev/null +++ b/tests/e2e/tests/misc/create-angular.ts @@ -0,0 +1,52 @@ +import { equal } from 'node:assert'; +import { join, resolve } from 'node:path'; +import { expectFileToExist, readFile, rimraf } from '../../utils/fs'; +import { getActivePackageManager } from '../../utils/packages'; +import { silentBun, silentNpm, silentPnpm, silentYarn } from '../../utils/process'; + +export default async function () { + const currentDirectory = process.cwd(); + const newDirectory = resolve('../'); + const projectName = 'test-project-create'; + + try { + process.chdir(newDirectory); + const packageManager = getActivePackageManager(); + + switch (packageManager) { + case 'npm': + await silentNpm('init', '@angular', projectName, '--', '--style=scss'); + + break; + case 'yarn': + await silentYarn('create', '@angular', projectName, '--style=scss'); + + break; + case 'bun': + await silentBun('create', '@angular', projectName, '--style=scss'); + + break; + case 'pnpm': + await silentPnpm('create', '@angular', projectName, '--style=scss'); + + break; + default: + throw new Error(`This test is not configured to use ${packageManager}.`); + } + + // Check that package manager has been configured based on the package manager used to invoke the create command. + const workspace = JSON.parse(await readFile(join(projectName, 'angular.json'))); + equal( + workspace.cli?.packageManager, + packageManager, + `Expected 'packageManager' option to be configured to ${packageManager}.`, + ); + + // Verify styles was create with correct extension. + await expectFileToExist(join(projectName, 'src/styles.scss')); + } finally { + await rimraf(projectName); + // Change directory back + process.chdir(currentDirectory); + } +} diff --git a/tests/e2e/tests/misc/dedupe-duplicate-modules.ts b/tests/e2e/tests/misc/dedupe-duplicate-modules.ts new file mode 100644 index 000000000000..5047021566eb --- /dev/null +++ b/tests/e2e/tests/misc/dedupe-duplicate-modules.ts @@ -0,0 +1,65 @@ +import { expectFileToMatch, rimraf, writeFile } from '../../utils/fs'; +import { gitClean } from '../../utils/git'; +import { installWorkspacePackages } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + try { + // Force duplicate modules + await updateJsonFile('package.json', (json) => { + json.dependencies = { + ...json.dependencies, + 'tslib': '^2.0.0', + 'tslib-1': 'npm:tslib@1.13.0', + 'tslib-1-copy': 'npm:tslib@1.13.0', + }; + }); + + await installWorkspacePackages(); + + await writeFile( + './src/main.ts', + ` + import { __assign as __assign_0 } from 'tslib'; + import { __assign as __assign_1 } from 'tslib-1'; + import { __assign as __assign_2 } from 'tslib-1-copy'; + + console.log({ + __assign_0, + __assign_1, + __assign_2, + }) + `, + ); + + const { stderr } = await ng( + 'build', + '--verbose', + '--no-vendor-chunk', + '--no-progress', + '--configuration=development', + ); + const outFile = 'dist/test-project/browser/main.js'; + + if (/\[DedupeModuleResolvePlugin\]:.+tslib-1-copy.+ -> .+tslib-1.+/.test(stderr)) { + await expectFileToMatch(outFile, './node_modules/tslib-1/tslib.es6.js'); + await expectToFail(() => + expectFileToMatch(outFile, './node_modules/tslib-1-copy/tslib.es6.js'), + ); + } else if (/\[DedupeModuleResolvePlugin\]:.+tslib-1.+ -> .+tslib-1-copy.+/.test(stderr)) { + await expectFileToMatch(outFile, './node_modules/tslib-1-copy/tslib.es6.js'); + await expectToFail(() => expectFileToMatch(outFile, './node_modules/tslib-1/tslib.es6.js')); + } else { + console.error(`\n\n\n${stderr}\n\n\n`); + throw new Error('Expected stderr to contain [DedupeModuleResolvePlugin] log for tslib.'); + } + + await expectFileToMatch(outFile, './node_modules/tslib/tslib.es6.mjs'); + } finally { + await rimraf('node_modules/tslib'); + await gitClean(); + await installWorkspacePackages(); + } +} diff --git a/tests/e2e/tests/misc/duplicate-command-line-option.ts b/tests/e2e/tests/misc/duplicate-command-line-option.ts new file mode 100644 index 000000000000..0042a363e156 --- /dev/null +++ b/tests/e2e/tests/misc/duplicate-command-line-option.ts @@ -0,0 +1,19 @@ +import { ng } from '../../utils/process'; +import { expectFileToExist } from '../../utils/fs'; + +export default async function () { + const { stderr } = await ng( + 'generate', + 'component', + 'test-component', + '--style=scss', + '--style=sass', + ); + + const warningMatch = `Option 'style' has been specified multiple times. The value 'sass' will be used`; + if (!stderr.includes(warningMatch)) { + throw new Error(`Expected stderr to contain: "${warningMatch}".`); + } + + await expectFileToExist('src/app/test-component/test-component.sass'); +} diff --git a/tests/e2e/tests/misc/es2015-nometa.ts b/tests/e2e/tests/misc/es2015-nometa.ts new file mode 100644 index 000000000000..c8fad0e07954 --- /dev/null +++ b/tests/e2e/tests/misc/es2015-nometa.ts @@ -0,0 +1,18 @@ +import { prependToFile, replaceInFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + await ng('generate', 'service', 'user-service'); + + // Update the application to use the new service + await prependToFile('src/app/app.ts', "import { UserService } from './user-service';"); + + await replaceInFile( + 'src/app/app.ts', + 'export class App {', + 'export class App {\n constructor(user: UserService) {}', + ); + + // Execute the application's tests with emitDecoratorMetadata disabled (default) + await ng('test', '--no-watch'); +} diff --git a/tests/e2e/tests/misc/forwardref-es2015.ts b/tests/e2e/tests/misc/forwardref-es2015.ts new file mode 100644 index 000000000000..48c28086cd0d --- /dev/null +++ b/tests/e2e/tests/misc/forwardref-es2015.ts @@ -0,0 +1,43 @@ +import { appendToFile, replaceInFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + // Update the application to use a forward reference + await replaceInFile( + 'src/app/app.ts', + "import { Component, signal } from '@angular/core';", + "import { Component, Inject, Injectable, forwardRef, signal } from '@angular/core';", + ); + await appendToFile('src/app/app.ts', '\n@Injectable() export class Lock { }\n'); + await replaceInFile( + 'src/app/app.ts', + 'export class App {', + 'export class App {\n constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {}', + ); + + // Update the application's unit tests to include the new injectable + await replaceInFile( + 'src/app/app.spec.ts', + "import { App } from './app';", + "import { App, Lock } from './app';", + ); + await replaceInFile( + 'src/app/app.spec.ts', + 'TestBed.configureTestingModule({', + 'TestBed.configureTestingModule({ providers: [Lock],', + ); + + // Execute the application's tests with emitDecoratorMetadata disabled (default) + await ng('test', '--no-watch'); + + // Turn on emitDecoratorMetadata + await replaceInFile( + 'tsconfig.json', + '"experimentalDecorators": true', + '"experimentalDecorators": true, "emitDecoratorMetadata": true', + ); + + // Execute the application's tests with emitDecoratorMetadata enabled + await expectToFail(() => ng('test', '--no-watch')); +} diff --git a/tests/e2e/tests/misc/invalid-schematic-dependencies.ts b/tests/e2e/tests/misc/invalid-schematic-dependencies.ts new file mode 100644 index 000000000000..88300951965e --- /dev/null +++ b/tests/e2e/tests/misc/invalid-schematic-dependencies.ts @@ -0,0 +1,60 @@ +import { join } from 'node:path'; +import { expectFileToMatch } from '../../utils/fs'; +import { + execWithEnv, + extractCIAndInfraEnv, + extractNpmEnv, + ng, + silentNpm, +} from '../../utils/process'; +import { getActivePackageManager, installPackage, uninstallPackage } from '../../utils/packages'; +import { isPrereleaseCli } from '../../utils/project'; +import { appendFile, writeFile } from 'node:fs/promises'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + // Must publish old version to local registry to allow install. This is especially important + // for release commits as npm will try to request tooling packages that are not on the npm registry yet + await publishOutdated('@schematics/angular@7'); + await publishOutdated('@angular-devkit/core@7'); + await publishOutdated('@angular-devkit/schematics@7'); + + // Install outdated and incompatible version + await installPackage('@schematics/angular@7'); + + const isPrerelease = await isPrereleaseCli(); + const tag = isPrerelease ? '@next' : ''; + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + await ng('add', `@angular/material${tag}`, '--skip-confirmation'); + await expectFileToMatch('package.json', /@angular\/material/); + + // Clean up existing cdk package + // Not doing so can cause adding material to fail if an incompatible cdk is present + await uninstallPackage('@angular/cdk'); +} + +async function publishOutdated(npmSpecifier: string): Promise { + const npmrc = join(getGlobalVariable('tmp-root'), '.npmrc-publish'); + const testRegistry = (getGlobalVariable('package-registry') as string).replace(/^\w+:/, ''); + await writeFile( + npmrc, + ` + ${testRegistry.replace(/^https?:/, '')}/:_authToken=fake-secret + `, + ); + + const { stdout: stdoutPack } = await silentNpm( + 'pack', + npmSpecifier, + '--registry=https://registry.npmjs.org', + ); + + await execWithEnv('npm', ['publish', stdoutPack.trim(), '--tag=outdated'], { + ...extractNpmEnv(), + ...extractCIAndInfraEnv(), + 'NPM_CONFIG_USERCONFIG': npmrc, + }); +} diff --git a/tests/e2e/tests/misc/loaders-resolution.ts b/tests/e2e/tests/misc/loaders-resolution.ts new file mode 100644 index 000000000000..b411ab60514a --- /dev/null +++ b/tests/e2e/tests/misc/loaders-resolution.ts @@ -0,0 +1,45 @@ +import { createDir, moveFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { assertIsError } from '../../utils/utils'; + +export default async function () { + await createDir('node_modules/@angular-devkit/build-angular/node_modules'); + let originalInRootNodeModules = true; + + try { + await moveFile( + 'node_modules/@ngtools', + 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools', + ); + } catch (e) { + assertIsError(e); + + if (e.code !== 'ENOENT') { + throw e; + } + + // In some cases due to module resolution '@ngtools' might already been under `@angular-devkit/build-angular`. + originalInRootNodeModules = false; + await moveFile( + 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools', + 'node_modules/@ngtools', + ); + } + + await ng('build', '--configuration=development'); + + // Move it back. + await moveBack(originalInRootNodeModules); +} + +function moveBack(originalInRootNodeModules: Boolean): Promise { + return originalInRootNodeModules + ? moveFile( + 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools', + 'node_modules/@ngtools', + ) + : moveFile( + 'node_modules/@ngtools', + 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools', + ); +} diff --git a/tests/e2e/tests/misc/module-resolution/module-resolution-core-mapping.ts b/tests/e2e/tests/misc/module-resolution/module-resolution-core-mapping.ts new file mode 100644 index 000000000000..d2710b07b8be --- /dev/null +++ b/tests/e2e/tests/misc/module-resolution/module-resolution-core-mapping.ts @@ -0,0 +1,41 @@ +import { createDir, moveFile } from '../../../utils/fs'; +import { ng } from '../../../utils/process'; +import { updateJsonFile } from '../../../utils/project'; +import { expectToFail } from '../../../utils/utils'; + +export default async function () { + await updateJsonFile('tsconfig.json', (tsconfig) => { + tsconfig.compilerOptions.paths = { + '*': ['./node_modules/*'], + }; + }); + await ng('build', '--configuration=development'); + + await createDir('xyz'); + await moveFile('node_modules/@angular/platform-browser', 'xyz/platform-browser'); + await expectToFail(() => ng('build', '--configuration=development')); + + await updateJsonFile('tsconfig.json', (tsconfig) => { + tsconfig.compilerOptions.paths = { + '@angular/platform-browser': ['./xyz/platform-browser'], + }; + }); + await ng('build', '--configuration=development'); + + await updateJsonFile('tsconfig.json', (tsconfig) => { + tsconfig.compilerOptions.paths = { + '*': ['./node_modules/*'], + '@angular/platform-browser': ['./xyz/platform-browser'], + }; + }); + await ng('build', '--configuration=development'); + + await updateJsonFile('tsconfig.json', (tsconfig) => { + tsconfig.compilerOptions.paths = { + '@angular/platform-browser': ['./xyz/platform-browser'], + '*': ['./node_modules/*'], + }; + }); + await ng('build', '--configuration=development'); + await moveFile('xyz/platform-browser', 'node_modules/@angular/platform-browser'); +} diff --git a/tests/e2e/tests/misc/multiple-targets.ts b/tests/e2e/tests/misc/multiple-targets.ts new file mode 100644 index 000000000000..a99a37d54b06 --- /dev/null +++ b/tests/e2e/tests/misc/multiple-targets.ts @@ -0,0 +1,9 @@ +import { expectFileToExist } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + await ng('generate', 'app', 'secondary-app'); + await ng('build', 'secondary-app', '--configuration=development'); + await expectFileToExist('dist/secondary-app/browser/index.html'); + await expectFileToExist('dist/secondary-app/browser/main.js'); +} diff --git a/tests/e2e/tests/misc/negated-boolean-options.ts b/tests/e2e/tests/misc/negated-boolean-options.ts new file mode 100644 index 000000000000..377967785496 --- /dev/null +++ b/tests/e2e/tests/misc/negated-boolean-options.ts @@ -0,0 +1,18 @@ +import { copyAssets } from '../../utils/assets'; +import { execAndWaitForOutputToMatch } from '../../utils/process'; + +export default async function () { + await copyAssets('schematic-boolean-option-negated', 'schematic-boolean-option-negated'); + + await execAndWaitForOutputToMatch( + 'ng', + ['generate', './schematic-boolean-option-negated:test', '--no-watch'], + /noWatch: true/, + ); + + await execAndWaitForOutputToMatch( + 'ng', + ['generate', './schematic-boolean-option-negated:test', '--watch'], + /noWatch: false/, + ); +} diff --git a/tests/legacy-cli/e2e/tests/misc/nested-schematic-packages.ts b/tests/e2e/tests/misc/nested-schematic-packages.ts similarity index 100% rename from tests/legacy-cli/e2e/tests/misc/nested-schematic-packages.ts rename to tests/e2e/tests/misc/nested-schematic-packages.ts diff --git a/tests/legacy-cli/e2e/tests/misc/supported-angular.ts b/tests/e2e/tests/misc/supported-angular.ts similarity index 78% rename from tests/legacy-cli/e2e/tests/misc/supported-angular.ts rename to tests/e2e/tests/misc/supported-angular.ts index 271e8663c4c4..d5299485bb7f 100644 --- a/tests/legacy-cli/e2e/tests/misc/supported-angular.ts +++ b/tests/e2e/tests/misc/supported-angular.ts @@ -4,7 +4,6 @@ import { readFile, writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; - export default async function () { if (getGlobalVariable('argv')['ng-snapshots']) { // The snapshots job won't work correctly because it doesn't use semver for Angular. @@ -25,12 +24,14 @@ export default async function () { // Major should succeed, but we don't need to test it here since it's tested everywhere else. // Major+1 and -1 should fail architect commands, but allow other commands. - await fakeCoreVersion(cliMajor + 1); - await expectToFail(() => ng('build'), 'Should fail Major+1'); - await ng('version'); - await fakeCoreVersion(cliMajor - 1); - await ng('version'); - - // Restore the original core package.json. - await writeFile(angularCorePkgPath, originalAngularCorePkgJson); + try { + await fakeCoreVersion(cliMajor + 1); + await expectToFail(() => ng('build'), 'Should fail Major+1'); + await ng('version'); + await fakeCoreVersion(cliMajor - 1); + await ng('version'); + } finally { + // Restore the original core package.json. + await writeFile(angularCorePkgPath, originalAngularCorePkgJson); + } } diff --git a/tests/e2e/tests/misc/target-default-configuration.ts b/tests/e2e/tests/misc/target-default-configuration.ts new file mode 100644 index 000000000000..d5f381b2db97 --- /dev/null +++ b/tests/e2e/tests/misc/target-default-configuration.ts @@ -0,0 +1,35 @@ +import { expectFileToExist } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + await updateJsonFile('angular.json', (workspace) => { + const build = workspace.projects['test-project'].architect.build; + build.defaultConfiguration = undefined; + build.options = { + ...build.options, + optimization: false, + buildOptimizer: false, + outputHashing: 'none', + sourceMap: true, + }; + }); + + await ng('build'); + await expectFileToExist('dist/test-project/browser/main.js'); + await expectFileToExist('dist/test-project/browser/main.js.map'); + + // Add new configuration and set "defaultConfiguration" + await updateJsonFile('angular.json', (workspace) => { + const build = workspace.projects['test-project'].architect.build; + build.defaultConfiguration = 'foo'; + build.configurations.foo = { + sourceMap: false, + }; + }); + + await ng('build'); + await expectFileToExist('dist/test-project/browser/main.js'); + await expectToFail(() => expectFileToExist('dist/test-project/browser/main.js.map')); +} diff --git a/tests/legacy-cli/e2e/tests/misc/trace-resolution.ts b/tests/e2e/tests/misc/trace-resolution.ts similarity index 86% rename from tests/legacy-cli/e2e/tests/misc/trace-resolution.ts rename to tests/e2e/tests/misc/trace-resolution.ts index cce9ef382bf5..0827223e58a0 100644 --- a/tests/legacy-cli/e2e/tests/misc/trace-resolution.ts +++ b/tests/e2e/tests/misc/trace-resolution.ts @@ -2,7 +2,7 @@ import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; export default async function () { - await updateJsonFile('tsconfig.json', tsconfig => { + await updateJsonFile('tsconfig.json', (tsconfig) => { tsconfig.compilerOptions.traceResolution = true; }); @@ -11,7 +11,7 @@ export default async function () { throw new Error(`Modules resolutions must be printed when 'traceResolution' is enabled.`); } - await updateJsonFile('tsconfig.json', tsconfig => { + await updateJsonFile('tsconfig.json', (tsconfig) => { tsconfig.compilerOptions.traceResolution = false; }); diff --git a/tests/legacy-cli/e2e/tests/misc/trusted-types.ts b/tests/e2e/tests/misc/trusted-types.ts similarity index 76% rename from tests/legacy-cli/e2e/tests/misc/trusted-types.ts rename to tests/e2e/tests/misc/trusted-types.ts index 4efb8b9f70e3..325ee521fe6d 100644 --- a/tests/legacy-cli/e2e/tests/misc/trusted-types.ts +++ b/tests/e2e/tests/misc/trusted-types.ts @@ -3,26 +3,21 @@ * Copyright Google LLC All Rights 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 { appendToFile, prependToFile, replaceInFile, writeFile } from '../../utils/fs'; +import { replaceInFile, writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; export default async function () { - // Add app routing. - // This is done automatically on a new app with --routing. - await prependToFile('src/app/app.module.ts', `import { RouterModule } from '@angular/router';`); + // Add lazy route. + await ng('generate', 'component', 'lazy'); await replaceInFile( - 'src/app/app.module.ts', - `imports: [`, - `imports: [ RouterModule.forRoot([]),`, + 'src/app/app.routes.ts', + 'routes: Routes = [];', + `routes: Routes = [{path: 'lazy', loadComponent: () => import('./lazy/lazy').then(c => c.Lazy)}];`, ); - await appendToFile('src/app/app.component.html', ''); - - // Add lazy route. - await ng('generate', 'module', 'lazy', '--route', 'lazy', '--module', 'app.module'); // Add lazy route e2e await writeFile( diff --git a/tests/e2e/tests/misc/update-git-clean-subdirectory.ts b/tests/e2e/tests/misc/update-git-clean-subdirectory.ts new file mode 100644 index 000000000000..733a91164019 --- /dev/null +++ b/tests/e2e/tests/misc/update-git-clean-subdirectory.ts @@ -0,0 +1,25 @@ +import { getGlobalVariable } from '../../utils/env'; +import { createDir, writeFile } from '../../utils/fs'; +import { ng, silentGit } from '../../utils/process'; +import { prepareProjectForE2e } from '../../utils/project'; + +export default async function () { + process.chdir(getGlobalVariable('projects-root')); + + await createDir('./subdirectory'); + process.chdir('./subdirectory'); + + await silentGit('init', '.'); + + await ng('new', 'subdirectory-test-project', '--skip-install', '--test-runner', 'karma'); + process.chdir('./subdirectory-test-project'); + await prepareProjectForE2e('subdirectory-test-project'); + + await writeFile('../added.ts', "console.log('created');\n"); + await silentGit('add', '../added.ts'); + + const { stderr } = await ng('update', '@angular/cli'); + if (stderr && stderr.includes('Repository is not clean.')) { + throw new Error('Expected clean repository'); + } +} diff --git a/tests/legacy-cli/e2e/tests/misc/update-git-clean.ts b/tests/e2e/tests/misc/update-git-clean.ts similarity index 77% rename from tests/legacy-cli/e2e/tests/misc/update-git-clean.ts rename to tests/e2e/tests/misc/update-git-clean.ts index 0026fff5c537..c992c695c4e3 100644 --- a/tests/legacy-cli/e2e/tests/misc/update-git-clean.ts +++ b/tests/e2e/tests/misc/update-git-clean.ts @@ -2,8 +2,8 @@ import { appendToFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; -export default async function() { - await appendToFile('src/main.ts', 'console.log(\'changed\');\n'); +export default async function () { + await appendToFile('src/main.ts', "console.log('changed');\n"); const { message } = await expectToFail(() => ng('update', '@angular/cli')); if (!message || !message.includes('Repository is not clean.')) { diff --git a/tests/e2e/tests/misc/version.ts b/tests/e2e/tests/misc/version.ts new file mode 100644 index 000000000000..4ad57adc9726 --- /dev/null +++ b/tests/e2e/tests/misc/version.ts @@ -0,0 +1,22 @@ +import { deleteFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; + +export default async function () { + const { stdout: commandOutput } = await ng('version'); + + if (commandOutput.includes(process.versions.node + ' (Unsupported)')) { + throw new Error('Node version should not show unsupported entry'); + } + + if (commandOutput.includes('Warning: The current version of Node ')) { + throw new Error('Node support warning should not be shown'); + } + + // doesn't fail on a project with missing angular.json + await deleteFile('angular.json'); + await ng('version'); + + // Doesn't fail outside a project. + process.chdir('/'); + await ng('version'); +} diff --git a/tests/e2e/tests/misc/workspace-verification.ts b/tests/e2e/tests/misc/workspace-verification.ts new file mode 100644 index 000000000000..bf55841a9398 --- /dev/null +++ b/tests/e2e/tests/misc/workspace-verification.ts @@ -0,0 +1,14 @@ +import { deleteFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + return ( + ng('generate', 'component', 'foo', '--dry-run') + .then(() => deleteFile('angular.json')) + // fails because it needs to be inside a project + // without a workspace file + .then(() => expectToFail(() => ng('generate', 'component', 'foo', '--dry-run'))) + .then(() => ng('version')) + ); +} diff --git a/tests/e2e/tests/protractor/test-fails.ts b/tests/e2e/tests/protractor/test-fails.ts new file mode 100644 index 000000000000..e185975b6391 --- /dev/null +++ b/tests/e2e/tests/protractor/test-fails.ts @@ -0,0 +1,15 @@ +import { execAndCaptureError } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function () { + // Revert the `private-protractor` builder name back to the previous `protractor`. + await updateJsonFile('angular.json', (config) => { + config.projects['test-project'].architect['e2e'].builder = + '@angular-devkit/build-angular:protractor'; + }); + + const error = await execAndCaptureError('ng', ['e2e']); + if (!error.message.includes('Protractor has reached end-of-life')) { + throw new Error(`Protractor did not fail with an appropriate message. Got:\n${error.message}`); + } +} diff --git a/tests/e2e/tests/schematics_cli/basic.ts b/tests/e2e/tests/schematics_cli/basic.ts new file mode 100644 index 000000000000..62128e1e32db --- /dev/null +++ b/tests/e2e/tests/schematics_cli/basic.ts @@ -0,0 +1,37 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { exec, execAndWaitForOutputToMatch, silentNpm } from '../../utils/process'; +import { rimraf } from '../../utils/fs'; + +export default async function () { + // setup + const argv = getGlobalVariable('argv'); + if (argv.noglobal) { + return; + } + + await silentNpm('install', '-g', '@angular-devkit/schematics-cli'); + await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); + + const startCwd = process.cwd(); + const schematicPath = join(startCwd, 'test-schematic'); + + try { + // create blank schematic + await exec('schematics', 'schematic', '--name', 'test-schematic'); + + process.chdir(join(startCwd, 'test-schematic')); + await execAndWaitForOutputToMatch( + 'schematics', + ['.:', '--list-schematics'], + /my-full-schematic/, + ); + } finally { + // restore path + process.chdir(startCwd); + await Promise.all([ + rimraf(schematicPath), + silentNpm('uninstall', '-g', '@angular-devkit/schematics-cli'), + ]); + } +} diff --git a/tests/e2e/tests/schematics_cli/blank-test.ts b/tests/e2e/tests/schematics_cli/blank-test.ts new file mode 100644 index 000000000000..4087f7476db9 --- /dev/null +++ b/tests/e2e/tests/schematics_cli/blank-test.ts @@ -0,0 +1,34 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { exec, silentNpm } from '../../utils/process'; +import { rimraf } from '../../utils/fs'; + +export default async function () { + // setup + const argv = getGlobalVariable('argv'); + if (argv.noglobal) { + return; + } + + await silentNpm('install', '-g', '@angular-devkit/schematics-cli'); + await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); + + const startCwd = process.cwd(); + const schematicPath = join(startCwd, 'test-schematic'); + + try { + // create schematic + await exec('schematics', 'blank', '--name', 'test-schematic'); + + process.chdir(schematicPath); + + await silentNpm('test'); + } finally { + // restore path + process.chdir(startCwd); + await Promise.all([ + rimraf(schematicPath), + silentNpm('uninstall', '-g', '@angular-devkit/schematics-cli'), + ]); + } +} diff --git a/tests/e2e/tests/schematics_cli/schematic-test.ts b/tests/e2e/tests/schematics_cli/schematic-test.ts new file mode 100644 index 000000000000..7d7d26e7304b --- /dev/null +++ b/tests/e2e/tests/schematics_cli/schematic-test.ts @@ -0,0 +1,34 @@ +import { join } from 'node:path'; +import { getGlobalVariable } from '../../utils/env'; +import { exec, silentNpm } from '../../utils/process'; +import { rimraf } from '../../utils/fs'; + +export default async function () { + // setup + const argv = getGlobalVariable('argv'); + if (argv.noglobal) { + return; + } + + await silentNpm('install', '-g', '@angular-devkit/schematics-cli'); + await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); + + const startCwd = process.cwd(); + const schematicPath = join(startCwd, 'test-schematic'); + + try { + // create schematic + await exec('schematics', 'schematic', '--name', 'test-schematic'); + + process.chdir(schematicPath); + + await silentNpm('test'); + } finally { + // restore path + process.chdir(startCwd); + await Promise.all([ + rimraf(schematicPath), + silentNpm('uninstall', '-g', '@angular-devkit/schematics-cli'), + ]); + } +} diff --git a/tests/e2e/tests/test/karma-junit-output.ts b/tests/e2e/tests/test/karma-junit-output.ts new file mode 100644 index 000000000000..056adea26ab3 --- /dev/null +++ b/tests/e2e/tests/test/karma-junit-output.ts @@ -0,0 +1,27 @@ +import { expectFileMatchToExist, replaceInFile } from '../../utils/fs'; +import { installPackage } from '../../utils/packages'; +import { silentNg } from '../../utils/process'; + +const E2E_CUSTOM_LAUNCHER = ` + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], + }, + }, + restartOnFileChange: true, +`; + +export default async function () { + await installPackage('karma-junit-reporter'); + await silentNg('generate', 'config', 'karma'); + + await replaceInFile('karma.conf.js', 'karma-jasmine-html-reporter', 'karma-junit-reporter'); + await replaceInFile('karma.conf.js', `'kjhtml'`, `'junit'`); + + await replaceInFile('karma.conf.js', `restartOnFileChange: true`, E2E_CUSTOM_LAUNCHER); + + await silentNg('test', '--no-watch'); + + await expectFileMatchToExist('.', /TESTS\-.+\.xml/); +} diff --git a/tests/e2e/tests/test/test-code-coverage-exclude.ts b/tests/e2e/tests/test/test-code-coverage-exclude.ts new file mode 100644 index 000000000000..808bcb59e2d9 --- /dev/null +++ b/tests/e2e/tests/test/test-code-coverage-exclude.ts @@ -0,0 +1,26 @@ +import { getGlobalVariable } from '../../utils/env'; +import { expectFileToExist, rimraf } from '../../utils/fs'; +import { silentNg } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; + const coverageOptionName = isWebpack ? '--code-coverage' : '--coverage'; + + // This test is already in build-angular, but that doesn't run on Windows. + await silentNg('test', '--no-watch', coverageOptionName); + await expectFileToExist('coverage/test-project/app.ts.html'); + // Delete coverage directory + await rimraf('coverage'); + + await silentNg( + 'test', + '--no-watch', + coverageOptionName, + `${coverageOptionName}-exclude='src/**/app.ts'`, + ); + + // Doesn't include excluded. + await expectFileToExist('coverage/test-project/index.html'); + await expectToFail(() => expectFileToExist('coverage/test-project/app.ts.html')); +} diff --git a/tests/e2e/tests/test/test-environment.ts b/tests/e2e/tests/test/test-environment.ts new file mode 100644 index 000000000000..1670cb246521 --- /dev/null +++ b/tests/e2e/tests/test/test-environment.ts @@ -0,0 +1,71 @@ +import { ng } from '../../utils/process'; +import { writeFile, writeMultipleFiles } from '../../utils/fs'; +import { updateJsonFile } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; + + // Tests run in 'dev' environment by default. + await writeMultipleFiles({ + 'src/environment.prod.ts': ` + export const environment = { + production: true + };`, + 'src/environment.ts': ` + export const environment = { + production: false + }; + `, + 'src/app/environment.spec.ts': ` + import { environment } from '../environment'; + + describe('Test environment', () => { + it('should have production disabled', () => { + expect(environment.production).toBe(false); + }); + }); + `, + }); + + await ng('test', '--watch=false'); + + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + appArchitect[isWebpack ? 'test' : 'build'].configurations = { + production: { + fileReplacements: [ + { + replace: 'src/environment.ts', + with: 'src/environment.prod.ts', + }, + ], + }, + }; + if (!isWebpack) { + appArchitect.test.options ??= {}; + appArchitect.test.options.buildTarget = '::production'; + } + }); + + // Tests can run in different environment. + + await writeFile( + 'src/app/environment.spec.ts', + ` + import { environment } from '../environment'; + + describe('Test environment', () => { + it('should have production enabled', () => { + expect(environment.production).toBe(true); + }); + }); + `, + ); + + if (isWebpack) { + await ng('test', '--watch=false', '--configuration=production'); + } else { + await ng('test', '--watch=false'); + } +} diff --git a/tests/e2e/tests/test/test-fail-single-run.ts b/tests/e2e/tests/test/test-fail-single-run.ts new file mode 100644 index 000000000000..d2054e7c37ee --- /dev/null +++ b/tests/e2e/tests/test/test-fail-single-run.ts @@ -0,0 +1,12 @@ +import { ng } from '../../utils/process'; +import { writeFile } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; + +export default function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + // Fails on single run with broken compilation. + return writeFile('src/app.spec.ts', '

definitely not typescript

').then(() => + expectToFail(() => ng('test', '--watch=false')), + ); +} diff --git a/tests/e2e/tests/test/test-include-glob.ts b/tests/e2e/tests/test/test-include-glob.ts new file mode 100644 index 000000000000..5dc55edbf8c7 --- /dev/null +++ b/tests/e2e/tests/test/test-include-glob.ts @@ -0,0 +1,5 @@ +import { ng } from '../../utils/process'; + +export default async function () { + await ng('test', '--no-watch', `--include='**/*.spec.ts'`); +} diff --git a/tests/e2e/tests/test/test-jasmine-clock.ts b/tests/e2e/tests/test/test-jasmine-clock.ts new file mode 100644 index 000000000000..37b164ff5914 --- /dev/null +++ b/tests/e2e/tests/test/test-jasmine-clock.ts @@ -0,0 +1,33 @@ +import { ng } from '../../utils/process'; +import { writeFile } from '../../utils/fs'; + +export default async function () { + await writeFile( + 'src/app/app.spec.ts', + ` + import { TestBed } from '@angular/core/testing'; + import { App } from './app'; + + describe('App', () => { + beforeAll(() => { + jasmine.clock().install(); + }); + + afterAll(() => { + jasmine.clock().uninstall(); + }); + + beforeEach(() => TestBed.configureTestingModule({ + imports: [App] + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + expect(fixture.componentInstance).toBeTruthy(); + }); + }); + `, + ); + + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/test/test-scripts.ts b/tests/e2e/tests/test/test-scripts.ts new file mode 100644 index 000000000000..1537cdddf349 --- /dev/null +++ b/tests/e2e/tests/test/test-scripts.ts @@ -0,0 +1,75 @@ +import { getGlobalVariable } from '../../utils/env'; +import { writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + // TODO(architect): Delete this test. It is now in devkit/build-angular. + + await ng('test', '--watch=false'); + + // prepare global scripts test files + await writeMultipleFiles({ + 'src/string-script.js': `globalThis.stringScriptGlobal = 'string-scripts.js';`, + 'src/input-script.js': `globalThis.inputScriptGlobal = 'input-scripts.js';`, + 'src/typings.d.ts': ` + declare var stringScriptGlobal: any; + declare var inputScriptGlobal: any; + `, + 'src/app/app.ts': ` + import { Component } from '@angular/core'; + + @Component({ selector: 'app-root', template: '', standalone: false }) + export class App { + stringScriptGlobalProp = stringScriptGlobal; + inputScriptGlobalProp = inputScriptGlobal; + } + `, + 'src/app/app.spec.ts': ` + import { TestBed } from '@angular/core/testing'; + import { App } from './app'; + + describe('App', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [App] + })); + + it('should have access to string-script.js', () => { + let app = TestBed.createComponent(App).debugElement.componentInstance; + expect(app.stringScriptGlobalProp).toEqual('string-scripts.js'); + }); + + it('should have access to input-script.js', () => { + let app = TestBed.createComponent(App).debugElement.componentInstance; + expect(app.inputScriptGlobalProp).toEqual('input-scripts.js'); + }); + }); + + describe('Spec', () => { + it('should have access to string-script.js', () => { + expect(stringScriptGlobal).toBe('string-scripts.js'); + }); + + it('should have access to input-script.js', () => { + expect(inputScriptGlobal).toBe('input-scripts.js'); + }); + }); + `, + }); + + // should fail because the global scripts were not added to scripts array + await expectToFail(() => ng('test', '--watch=false')); + + const isWebpack = !getGlobalVariable('argv')['esbuild']; + await updateJsonFile('angular.json', (workspaceJson) => { + const appArchitect = workspaceJson.projects['test-project'].architect; + appArchitect[isWebpack ? 'test' : 'build'].options.scripts = [ + { input: 'src/string-script.js' }, + { input: 'src/input-script.js' }, + ]; + }); + + // should pass now + await ng('test', '--watch=false'); +} diff --git a/tests/e2e/tests/test/test-sourcemap.ts b/tests/e2e/tests/test/test-sourcemap.ts new file mode 100644 index 000000000000..6c1cf16cd7b3 --- /dev/null +++ b/tests/e2e/tests/test/test-sourcemap.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert'; +import { writeFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { assertIsError } from '../../utils/utils'; +import { updateJsonFile } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + const isWebpack = !getGlobalVariable('argv')['esbuild']; + + await writeFile( + 'src/app/app.spec.ts', + ` + it('should fail', () => { + expect(undefined).toBeTruthy(); + }); + `, + ); + + // when sourcemaps are 'on' the stacktrace will point to the spec.ts file. + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + if (isWebpack) { + appArchitect['test'].options.sourceMap = true; + } else { + appArchitect['build'].configurations.development.sourceMap = true; + } + }); + try { + await ng('test', '--no-watch'); + throw new Error('ng test should have failed.'); + } catch (error) { + assertIsError(error); + assert.match(error.message, /\(src\/app\/app\.spec\.ts:3:27/); + assert.doesNotMatch(error.message, /_karma_webpack_/); + } + + // when sourcemaps are 'off' the stacktrace won't point to the spec.ts file. + await updateJsonFile('angular.json', (configJson) => { + const appArchitect = configJson.projects['test-project'].architect; + if (isWebpack) { + appArchitect['test'].options.sourceMap = false; + } else { + appArchitect['build'].configurations.development.sourceMap = false; + } + }); + try { + await ng('test', '--no-watch'); + throw new Error('ng test should have failed.'); + } catch (error) { + assertIsError(error); + assert.match(error.message, /main\.js/); + } +} diff --git a/tests/e2e/tests/update/update-application-builder.ts b/tests/e2e/tests/update/update-application-builder.ts new file mode 100644 index 000000000000..2769fe943138 --- /dev/null +++ b/tests/e2e/tests/update/update-application-builder.ts @@ -0,0 +1,36 @@ +import { match } from 'node:assert'; +import { createProjectFromAsset } from '../../utils/assets'; +import { + expectFileMatchToExist, + expectFileNotToExist, + expectFileToExist, + expectFileToMatch, +} from '../../utils/fs'; +import { execAndWaitForOutputToMatch, ng, noSilentNg } from '../../utils/process'; +import { findFreePort } from '../../utils/network'; + +export default async function () { + await createProjectFromAsset('ssr-project-webpack', false, false); + await ng('update', `@angular/cli`, '--name=use-application-builder'); + + await Promise.all([ + expectFileNotToExist('tsconfig.server.json'), + expectFileToMatch('tsconfig.json', 'esModuleInterop'), + expectFileToMatch('src/server.ts', 'import.meta.url'), + ]); + + // Verify project now creates bundles + await noSilentNg('build', '--configuration=production'); + + await Promise.all([ + expectFileToExist('dist/ssr-project-webpack/server/server.mjs'), + expectFileMatchToExist('dist/ssr-project-webpack/browser', /main-[a-zA-Z0-9]{8}\.js/), + ]); + + // Verify that the app runs + const port = await findFreePort(); + await execAndWaitForOutputToMatch('ng', ['serve', '--port', String(port)], /complete\./); + const response = await fetch(`http://localhost:${port}/`); + const text = await response.text(); + match(text, /app is running!/); +} diff --git a/tests/e2e/tests/update/update-multiple-versions.ts b/tests/e2e/tests/update/update-multiple-versions.ts new file mode 100644 index 000000000000..6fecb7b15b58 --- /dev/null +++ b/tests/e2e/tests/update/update-multiple-versions.ts @@ -0,0 +1,43 @@ +import { createProjectFromAsset } from '../../utils/assets'; +import { setRegistry } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { isPrereleaseCli } from '../../utils/project'; +import { expectToFail } from '../../utils/utils'; + +export default async function () { + let restoreRegistry: (() => Promise) | undefined; + try { + restoreRegistry = await createProjectFromAsset('19.0-project', true); + await setRegistry(true); + + const extraArgs = ['--force']; + if (isPrereleaseCli()) { + extraArgs.push('--next'); + } + + // TODO(alanagius): investigate how to re-enable this. This is failing but it's correct since we are using the public registry. + // Update Angular from v13 to 14 + // const { stdout } = await ng('update', ...extraArgs); + // if (!/@angular\/core\s+13\.\d\.\d+ -> 14\.\d\.\d+\s+ng update @angular\/core@14/.test(stdout)) { + // // @angular/core 13.x.x -> 14.x.x ng update @angular/core@14 + // throw new Error( + // `Output didn't match "@angular/core 13.x.x -> 14.x.x ng update @angular/core@14". OUTPUT: \n` + + // stdout, + // ); + // } + + const { message } = await expectToFail(() => ng('update', '@angular/core', ...extraArgs)); + if ( + !message.includes( + `Updating multiple major versions of '@angular/core' at once is not supported`, + ) + ) { + throw new Error( + `Expected error message to include "Updating multiple major versions of '@angular/core' at once is not supported" but didn't. OUTPUT: \n` + + message, + ); + } + } finally { + await restoreRegistry?.(); + } +} diff --git a/tests/e2e/tests/update/update-secure-registry.ts b/tests/e2e/tests/update/update-secure-registry.ts new file mode 100644 index 000000000000..27b772799566 --- /dev/null +++ b/tests/e2e/tests/update/update-secure-registry.ts @@ -0,0 +1,46 @@ +import { exec, ng } from '../../utils/process'; +import { createNpmConfigForAuthentication } from '../../utils/registry'; +import { expectToFail } from '../../utils/utils'; +import { isPrereleaseCli } from '../../utils/project'; +import { getActivePackageManager } from '../../utils/packages'; +import assert from 'node:assert'; + +export default async function () { + // The environment variable has priority over the .npmrc + delete process.env['NPM_CONFIG_REGISTRY']; + const worksMessage = 'We analyzed your package.json'; + + const extraArgs: string[] = []; + if (isPrereleaseCli()) { + extraArgs.push('--next'); + } + + // Valid authentication token + await createNpmConfigForAuthentication(false); + const { stdout: stdout1 } = await ng('update', ...extraArgs); + if (!stdout1.includes(worksMessage)) { + throw new Error(`Expected stdout to contain "${worksMessage}"`); + } + + await createNpmConfigForAuthentication(true); + const { stdout: stdout2 } = await ng('update', ...extraArgs); + if (!stdout2.includes(worksMessage)) { + throw new Error(`Expected stdout to contain "${worksMessage}"`); + } + + // Invalid authentication token + await createNpmConfigForAuthentication(false, true); + await expectToFail(() => ng('update', ...extraArgs)); + + await createNpmConfigForAuthentication(true, true); + await expectToFail(() => ng('update', ...extraArgs)); + + if (getActivePackageManager() === 'yarn') { + // When running `ng update` using yarn (`yarn ng update`), yarn will set the `npm_config_registry` env variable to `https://registry.yarnpkg.com` + // Validate the registry in the RC is used. + await createNpmConfigForAuthentication(true, true); + + const error = await expectToFail(() => exec('yarn', 'ng', 'update', ...extraArgs)); + assert.match(error.message, /not allowed to access package/); + } +} diff --git a/tests/e2e/tests/update/update.ts b/tests/e2e/tests/update/update.ts new file mode 100644 index 000000000000..ae941762d6ca --- /dev/null +++ b/tests/e2e/tests/update/update.ts @@ -0,0 +1,94 @@ +import { appendFile } from 'node:fs/promises'; +import { createProjectFromAsset } from '../../utils/assets'; +import { expectFileMatchToExist } from '../../utils/fs'; +import { getActivePackageManager } from '../../utils/packages'; +import { ng, noSilentNg } from '../../utils/process'; +import { isPrereleaseCli, useCIChrome, useCIDefaults, getNgCLIVersion } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; + +export default async function () { + let restoreRegistry: (() => Promise) | undefined; + + try { + // We need to use the public registry because in the local NPM server we don't have + // older versions @angular/cli packages which would cause `npm install` during `ng update` to fail. + restoreRegistry = await createProjectFromAsset('19.0-project', true); + + // CLI project version + const cliMajorProjectVersion = 19; + + // If using npm, enable legacy peer deps mode to avoid defects in npm 7+'s peer dependency resolution + // Example error where 11.2.14 satisfies the SemVer range ^11.0.0 but still fails: + // npm ERR! Conflicting peer dependency: @angular/compiler-cli@11.2.14 + // npm ERR! node_modules/@angular/compiler-cli + // npm ERR! peer @angular/compiler-cli@"^11.0.0 || ^11.2.0-next" from @angular-devkit/build-angular@0.1102.19 + // npm ERR! node_modules/@angular-devkit/build-angular + // npm ERR! dev @angular-devkit/build-angular@"~0.1102.19" from the root project + if (getActivePackageManager() === 'npm') { + await appendFile('.npmrc', '\nlegacy-peer-deps=true'); + } + + // CLI current version. + const cliMajorVersion = getNgCLIVersion().major; + + for (let version = cliMajorProjectVersion + 1; version < cliMajorVersion; version++) { + // Run all the migrations until the current build major version - 1. + // Example: when the project is using CLI version 10 and the build CLI version is 14. + // We will run the following migrations: + // - 10 -> 11 + // - 11 -> 12 + // - 12 -> 13 + const { stdout } = await ng('update', `@angular/cli@${version}`, `@angular/core@${version}`); + if ( + !stdout.includes("Executing migrations of package '@angular/cli'") && + !stdout.includes("Optional migrations of package '@angular/cli'") + ) { + throw new Error('Update did not execute migrations for @angular/cli. OUTPUT: \n' + stdout); + } + if (!stdout.includes("Executing migrations of package '@angular/core'")) { + throw new Error('Update did not execute migrations for @angular/core. OUTPUT: \n' + stdout); + } + } + } finally { + await restoreRegistry?.(); + } + + // Update Angular current build + const extraUpdateArgs = isPrereleaseCli() ? ['--next', '--force'] : []; + + // For the latest/next release we purposely don't run `ng update @angular/core`. + + // During a major release when the branch version is bumped from `12.0.0-rc.x` to `12.0.0` there would be a period were in + // the local NPM registry `@angular/cli@latest` will point to `12.0.0`, but on the public NPM repository `@angular/core@latest` will be `11.2.x`. + + // This causes `ng update @angular/core` to fail because of mismatching peer dependencies. + + // The reason for this is because of our bumping and release strategy. When we release a major version on NPM we don't tag it + // `@latest` right away, but we wait for all teams to release their packages before doing so. While this is good because all team + // packages gets tagged with `@latest` at the same time. This is problematic for our CI, since we test against the public NPM repo and are dependent on tags. + + // NB: `ng update @angular/cli` will still cause `@angular/core` packages to be updated therefore we still test updating the core package without running the command. + + await ng('update', '@angular/cli', ...extraUpdateArgs); + + // Setup testing to use CI Chrome. + await useCIChrome('nineteen-project', './'); + await useCIDefaults('nineteen-project'); + + // Run CLI commands. + await ng('generate', 'component', 'my-comp'); + await ng('test', '--watch=false'); + + await executeBrowserTest({ + configuration: 'production', + expectedTitleText: 'Hello, nineteen-project', + }); + await executeBrowserTest({ + configuration: 'development', + expectedTitleText: 'Hello, nineteen-project', + }); + + // Verify project now creates bundles + await noSilentNg('build', '--configuration=production'); + await expectFileMatchToExist('dist/nineteen-project/browser', /main-[a-zA-Z0-9]{8}\.js/); +} diff --git a/tests/e2e/tests/vite/reuse-dep-optimization-cache.ts b/tests/e2e/tests/vite/reuse-dep-optimization-cache.ts new file mode 100644 index 000000000000..56ecdfee8cd0 --- /dev/null +++ b/tests/e2e/tests/vite/reuse-dep-optimization-cache.ts @@ -0,0 +1,41 @@ +import assert from 'node:assert'; +import { findFreePort } from '../../utils/network'; +import { + execAndWaitForOutputToMatch, + killAllProcesses, + ng, + waitForAnyProcessOutputToMatch, +} from '../../utils/process'; + +export default async function () { + await ng('cache', 'clean'); + await ng('cache', 'on'); + + const port = await findFreePort(); + const serveReady = execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port', `${port}`], + /Application bundle generation complete/, + // Use CI:0 to force caching + { ...process.env, DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, + ); + + // Note: Don't await `serveReady` before, as otherwise we might not see + // the dependencies optimized output. There is some debouncing for `ng serve` + // going on that could cause this. + await Promise.all([serveReady, waitForAnyProcessOutputToMatch(/dependencies optimized/, 10_000)]); + const response = await fetch(`http://localhost:${port}/main.js`); + + assert(response.ok, `Expected 'response.ok' to be 'true'.`); + + // Terminate the dev-server + await killAllProcesses(); + + await execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port=0'], + /Hash is consistent\. Skipping/, + // Use CI:0 to force caching + { ...process.env, DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, + ); +} diff --git a/tests/e2e/tests/vite/ssr-base-href.ts b/tests/e2e/tests/vite/ssr-base-href.ts new file mode 100644 index 000000000000..140f2582689a --- /dev/null +++ b/tests/e2e/tests/vite/ssr-base-href.ts @@ -0,0 +1,48 @@ +import assert from 'node:assert'; +import { ng } from '../../utils/process'; +import { replaceInFile } from '../../utils/fs'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, updateJsonFile, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await updateJsonFile('angular.json', (json) => { + json.projects['test-project'].architect.build.options['baseHref'] = '/base'; + }); + + await replaceInFile( + 'src/server.ts', + /express\(\);/, + `express(); + + app.use('/ping', (req, res) => { + return res.json({ pong: true }); + });`, + ); + + const port = await ngServe(); + + // Angular application and bundled should be affected by baseHref + await matchResponse(`http://localhost:${port}/base`, /ng-server-context/); + await matchResponse(`http://localhost:${port}/base/main.js`, /App/); + + // Server endpoint should not be affected by baseHref + await matchResponse(`http://localhost:${port}/ping`, /pong/); +} + +async function matchResponse(url: string, match: RegExp): Promise { + const response = await fetch(url); + const text = await response.text(); + + assert.match(text, match); +} diff --git a/tests/e2e/tests/vite/ssr-default.ts b/tests/e2e/tests/vite/ssr-default.ts new file mode 100644 index 000000000000..8b64a4b30f67 --- /dev/null +++ b/tests/e2e/tests/vite/ssr-default.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert'; +import { ng } from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Enable caching to test real development workflow. + await ng('cache', 'clean'); + await ng('cache', 'on'); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + const port = await ngServe(); + + // Verify the server is running and the API response is correct. + await validateResponse('/main.js', /bootstrapApplication/); + await validateResponse('/', /Hello,/); + await validateResponse('/unknown', /Cannot GET/, 404); + + async function validateResponse(pathname: string, match: RegExp, status = 200): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, status); + } +} diff --git a/tests/e2e/tests/vite/ssr-entry-express.ts b/tests/e2e/tests/vite/ssr-entry-express.ts new file mode 100644 index 000000000000..11cf671becbf --- /dev/null +++ b/tests/e2e/tests/vite/ssr-entry-express.ts @@ -0,0 +1,130 @@ +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { replaceInFile, writeMultipleFiles } from '../../utils/fs'; +import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + + export const routes: Routes = [ + { path: 'home', component: Home } + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { path: '**', renderMode: RenderMode.Server } + ]; + `, + 'src/server.ts': ` + import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; + import express from 'express'; + import { join } from 'node:path'; + + export function app(): express.Express { + const server = express(); + const browserDistFolder = join(import.meta.dirname, '../browser'); + const angularNodeAppEngine = new AngularNodeAppEngine(); + + server.use('/api/{*splat}', (req, res) => { + res.json({ hello: 'foo' }) + }); + + server.use(express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html' + })); + + server.use(async(req, res, next) => { + const response = await angularNodeAppEngine.handle(req); + if (response) { + writeResponseToNodeResponse(response, res); + } else { + next(); + } + }); + + return server; + } + + const server = app(); + if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000; + server.listen(port, (error) => { + if (error) { + throw error; + } + + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + export const reqHandler = createNodeRequestHandler(server); + `, + }); + + await silentNg('generate', 'component', 'home'); + + const port = await ngServe(); + + // Verify the server is running and the API response is correct. + await validateResponse('/main.js', /bootstrapApplication/); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /home works/); + + // Modify the home component and validate the change. + await modifyFileAndWaitUntilUpdated( + 'src/app/home/home.html', + 'home works', + 'yay home works!!!', + true, + ); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /yay home works/); + + // Modify the API response and validate the change. + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await validateResponse('/api/test', /bar/); + await validateResponse('/home', /yay home works/); + + async function validateResponse(pathname: string, match: RegExp): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, 200); + } +} + +async function modifyFileAndWaitUntilUpdated( + filePath: string, + searchValue: string, + replaceValue: string, + hmr = false, +): Promise { + await Promise.all([ + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), + setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), + ]); +} diff --git a/tests/e2e/tests/vite/ssr-entry-fastify.ts b/tests/e2e/tests/vite/ssr-entry-fastify.ts new file mode 100644 index 000000000000..c50e8c2200f6 --- /dev/null +++ b/tests/e2e/tests/vite/ssr-entry-fastify.ts @@ -0,0 +1,123 @@ +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { replaceInFile, writeMultipleFiles } from '../../utils/fs'; +import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await installPackage('fastify@5'); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + + export const routes: Routes = [ + { path: 'home', component: Home } + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { path: '**', renderMode: RenderMode.Server } + ]; + `, + 'src/server.ts': ` + import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; + import fastify from 'fastify'; + + export function app() { + const server = fastify(); + const angularNodeAppEngine = new AngularNodeAppEngine(); + server.get('/api/*', (req, reply) => reply.send({ hello: 'foo' })); + server.get('*', async (req, reply) => { + try { + const response = await angularNodeAppEngine.handle(req.raw); + if (response) { + await writeResponseToNodeResponse(response, reply.raw); + } else { + reply.callNotFound(); + } + } catch (error) { + reply.send(error); + } + }); + + return server; + } + + const server = app(); + if (isMainModule(import.meta.url)) { + const port = +(process.env['PORT'] || 4000); + server.listen({ port }, () => { + console.log(\`Fastify server listening on http://localhost:\${port}\`); + }); + } + + export const reqHandler = createNodeRequestHandler(async (req, res) => { + await server.ready(); + server.server.emit('request', req, res); + }); + `, + }); + + await silentNg('generate', 'component', 'home'); + + const port = await ngServe(); + + // Verify the server is running and the API response is correct. + await validateResponse('/main.js', /bootstrapApplication/); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /home works/); + + // Modify the home component and validate the change. + await modifyFileAndWaitUntilUpdated( + 'src/app/home/home.html', + 'home works', + 'yay home works!!!', + true, + ); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /yay home works/); + + // Modify the API response and validate the change. + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await validateResponse('/api/test', /bar/); + await validateResponse('/home', /yay home works/); + + async function validateResponse(pathname: string, match: RegExp): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, 200); + } +} + +async function modifyFileAndWaitUntilUpdated( + filePath: string, + searchValue: string, + replaceValue: string, + hmr = false, +): Promise { + await Promise.all([ + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), + setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), + ]); +} diff --git a/tests/e2e/tests/vite/ssr-entry-h3.ts b/tests/e2e/tests/vite/ssr-entry-h3.ts new file mode 100644 index 000000000000..4a4f91066000 --- /dev/null +++ b/tests/e2e/tests/vite/ssr-entry-h3.ts @@ -0,0 +1,114 @@ +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { replaceInFile, writeMultipleFiles } from '../../utils/fs'; +import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await installPackage('h3@1'); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + + export const routes: Routes = [ + { path: 'home', component: Home } + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { path: '**', renderMode: RenderMode.Server } + ]; + `, + 'src/server.ts': ` + import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; + import { createApp, createRouter, toWebHandler, defineEventHandler, toWebRequest } from 'h3'; + + export function app() { + const server = createApp(); + const router = createRouter(); + const angularAppEngine = new AngularAppEngine(); + + router.use( + '/api/**', + defineEventHandler(() => ({ hello: 'foo' })), + ); + + router.use( + '/**', + defineEventHandler((event) => angularAppEngine.handle(toWebRequest(event))), + ); + + server.use(router); + + return server; + } + + const server = app(); + + export const reqHandler = createRequestHandler(toWebHandler(server)); + `, + }); + + await silentNg('generate', 'component', 'home'); + + const port = await ngServe(); + + // Verify the server is running and the API response is correct. + await validateResponse('/main.js', /bootstrapApplication/); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /home works/); + + // Modify the home component and validate the change. + await modifyFileAndWaitUntilUpdated( + 'src/app/home/home.html', + 'home works', + 'yay home works!!!', + true, + ); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /yay home works/); + + // Modify the API response and validate the change. + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await validateResponse('/api/test', /bar/); + await validateResponse('/home', /yay home works/); + + async function validateResponse(pathname: string, match: RegExp): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, 200); + } +} + +async function modifyFileAndWaitUntilUpdated( + filePath: string, + searchValue: string, + replaceValue: string, + hmr = false, +): Promise { + await Promise.all([ + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), + setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), + ]); +} diff --git a/tests/e2e/tests/vite/ssr-entry-hono.ts b/tests/e2e/tests/vite/ssr-entry-hono.ts new file mode 100644 index 000000000000..0b0b8220fe6b --- /dev/null +++ b/tests/e2e/tests/vite/ssr-entry-hono.ts @@ -0,0 +1,106 @@ +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { replaceInFile, writeMultipleFiles } from '../../utils/fs'; +import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; +import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + await installPackage('hono@4'); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + + export const routes: Routes = [ + { path: 'home', component: Home } + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { path: '**', renderMode: RenderMode.Server } + ]; + `, + 'src/server.ts': ` + import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; + import { Hono } from 'hono'; + + export function app() { + const server = new Hono(); + const angularAppEngine = new AngularAppEngine(); + + server.get('/api/*', (c) => c.json({ hello: 'foo' })); + server.get('/*', async (c) => { + const res = await angularAppEngine.handle(c.req.raw); + return res || undefined + }); + + return server; + } + + const server = app(); + export const reqHandler = createRequestHandler(server.fetch); + `, + }); + + await silentNg('generate', 'component', 'home'); + + const port = await ngServe(); + + // Verify the server is running and the API response is correct. + await validateResponse('/main.js', /bootstrapApplication/); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /home works/); + + // Modify the home component and validate the change. + await modifyFileAndWaitUntilUpdated( + 'src/app/home/home.html', + 'home works', + 'yay home works!!!', + true, + ); + await validateResponse('/api/test', /foo/); + await validateResponse('/home', /yay home works/); + + // Modify the API response and validate the change. + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await validateResponse('/api/test', /bar/); + await validateResponse('/home', /yay home works/); + + async function validateResponse(pathname: string, match: RegExp): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, 200); + } +} + +async function modifyFileAndWaitUntilUpdated( + filePath: string, + searchValue: string, + replaceValue: string, + hmr = false, +): Promise { + await Promise.all([ + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), + setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), + ]); +} diff --git a/tests/e2e/tests/vite/ssr-error-stack.ts b/tests/e2e/tests/vite/ssr-error-stack.ts new file mode 100644 index 000000000000..8fce78b9e7ad --- /dev/null +++ b/tests/e2e/tests/vite/ssr-error-stack.ts @@ -0,0 +1,34 @@ +import { doesNotMatch, match } from 'node:assert'; +import { ng } from '../../utils/process'; +import { appendToFile } from '../../utils/fs'; +import { ngServe, useSha } from '../../utils/project'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; + +export default async function () { + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + // Create Error. + await appendToFile( + 'src/app/app.ts', + ` + (() => { + throw new Error('something happened!'); + })(); + `, + ); + + const port = await ngServe(); + const response = await fetch(`http://localhost:${port}/`); + const text = await response.text(); + + // The error is also sent in the browser, so we don't need to scrap the stderr. + match( + text, + /something happened.+at eval \(.+[\\/]+e2e-test[\\/]+test-project[\\/]+src[\\/]+app[\\/]+app\.ts:\d+:\d+\)/, + ); + doesNotMatch(text, /vite-root/); +} diff --git a/tests/e2e/tests/vite/ssr-new-dep-optimization.ts b/tests/e2e/tests/vite/ssr-new-dep-optimization.ts new file mode 100644 index 000000000000..d7b8a63813eb --- /dev/null +++ b/tests/e2e/tests/vite/ssr-new-dep-optimization.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert'; +import { + execAndWaitForOutputToMatch, + ng, + waitForAnyProcessOutputToMatch, +} from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; +import { readFile, writeFile } from '../../utils/fs'; +import { findFreePort } from '../../utils/network'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Enable caching to test real development workflow. + await ng('cache', 'clean'); + await ng('cache', 'on'); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port', port.toString()], + /Application bundle generation complete/, + { ...process.env, CI: '0', NO_COLOR: 'true' }, + ); + await validateResponse('/', /Hello,/); + + await Promise.all([ + waitForAnyProcessOutputToMatch( + /new dependencies optimized: @angular\/platform-browser\/animations\/async/, + 6000, + ), + writeFile( + 'src/app/app.config.ts', + ` + import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; + ${(await readFile('src/app/app.config.ts')).replace('provideRouter(routes),', 'provideAnimationsAsync(), provideRouter(routes),')} + `, + ), + ]); + + // Verify the app still works. + await validateResponse('/', /Hello,/); + + async function validateResponse(pathname: string, match: RegExp): Promise { + const response = await fetch(new URL(pathname, `http://localhost:${port}`)); + const text = await response.text(); + assert.match(text, match); + } +} diff --git a/tests/e2e/tests/vite/ssr-with-ssl.ts b/tests/e2e/tests/vite/ssr-with-ssl.ts new file mode 100644 index 000000000000..90518080f8f3 --- /dev/null +++ b/tests/e2e/tests/vite/ssr-with-ssl.ts @@ -0,0 +1,71 @@ +import { Agent } from 'undici'; +import assert from 'node:assert'; +import { writeMultipleFiles } from '../../utils/fs'; +import { ng, silentNg } from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { ngServe, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); + + await writeMultipleFiles({ + // Replace the template of app.ng.html as it makes it harder to debug + 'src/app/app.html': '', + 'src/app/app.routes.ts': ` + import { Routes } from '@angular/router'; + import { Home } from './home/home'; + + export const routes: Routes = [ + { path: 'home', component: Home } + ]; + `, + 'src/app/app.routes.server.ts': ` + import { RenderMode, ServerRoute } from '@angular/ssr'; + + export const serverRoutes: ServerRoute[] = [ + { path: '**', renderMode: RenderMode.Server } + ]; + `, + }); + + await silentNg('generate', 'component', 'home'); + + const port = await ngServe('--ssl'); + + // http 2 + await validateResponse('/main.js', /bootstrapApplication/, true); + await validateResponse('/home', /home works/, true); + + // http 1.1 + await validateResponse('/main.js', /bootstrapApplication/, false); + await validateResponse('/home', /home works/, false); + + async function validateResponse( + pathname: string, + match: RegExp, + allowH2: boolean, + ): Promise { + const response = await fetch(new URL(pathname, `https://localhost:${port}`), { + dispatcher: new Agent({ + connect: { + allowH2, + rejectUnauthorized: false, + }, + }), + }); + + const text = await response.text(); + assert.match(text, match); + assert.equal(response.status, 200); + } +} diff --git a/tests/e2e/tests/vitest/browser-no-globals.ts b/tests/e2e/tests/vitest/browser-no-globals.ts new file mode 100644 index 000000000000..b25f8168c5f7 --- /dev/null +++ b/tests/e2e/tests/vitest/browser-no-globals.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { writeFile } from '../../utils/fs'; +import { installPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; + +/** + * Allow `vitest` import in browser mode. + * @see https://github.com/angular/angular-cli/issues/31745 + */ +export default async function (): Promise { + await applyVitestBuilder(); + + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + + await writeFile( + 'src/app/app.spec.ts', + ` + import { test, expect } from 'vitest'; + + test('should pass', () => { + expect(true).toBe(true); + }); + `, + ); + + const { stdout } = await ng('test', '--browsers', 'ChromiumHeadless'); + + assert.match(stdout, /1 passed/, 'Expected 1 tests to pass.'); +} diff --git a/tests/e2e/tests/vitest/browser-playwright.ts b/tests/e2e/tests/vitest/browser-playwright.ts new file mode 100644 index 000000000000..fa9ec43aabf3 --- /dev/null +++ b/tests/e2e/tests/vitest/browser-playwright.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { ng } from '../../utils/process'; +import { installPackage } from '../../utils/packages'; +import { writeFile } from '../../utils/fs'; + +export default async function (): Promise { + await applyVitestBuilder(); + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + await ng('generate', 'component', 'my-comp'); + + await writeFile( + 'src/setup1.ts', + ` + import { getTestBed } from '@angular/core/testing'; + + getTestBed().configureTestingModule({}); + `, + ); + + const { stdout } = await ng( + 'test', + '--no-watch', + '--browsers', + 'chromiumHeadless', + '--setup-files', + 'src/setup1.ts', + ); + + assert.match(stdout, /2 passed/, 'Expected 2 tests to pass.'); +} diff --git a/tests/e2e/tests/vitest/browser-webdriverio.ts b/tests/e2e/tests/vitest/browser-webdriverio.ts new file mode 100644 index 000000000000..4ea1b913c3b0 --- /dev/null +++ b/tests/e2e/tests/vitest/browser-webdriverio.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { ng } from '../../utils/process'; +import { installPackage } from '../../utils/packages'; +import { writeFile } from '../../utils/fs'; + +export default async function (): Promise { + await applyVitestBuilder(); + await installPackage('webdriverio@9'); + await installPackage('@vitest/browser-webdriverio@4'); + + await ng('generate', 'component', 'my-comp'); + + await writeFile( + 'src/setup1.ts', + ` + import { getTestBed } from '@angular/core/testing'; + + getTestBed().configureTestingModule({}); + `, + ); + + const { stdout } = await ng( + 'test', + '--no-watch', + '--browsers', + 'chromeHeadless', + '--setup-files', + 'src/setup1.ts', + ); + + assert.match(stdout, /2 passed/, 'Expected 2 tests to pass.'); +} diff --git a/tests/e2e/tests/vitest/component.ts b/tests/e2e/tests/vitest/component.ts new file mode 100644 index 000000000000..421587892196 --- /dev/null +++ b/tests/e2e/tests/vitest/component.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { ng } from '../../utils/process'; + +export default async function (): Promise { + await applyVitestBuilder(); + await ng('generate', 'component', 'my-comp'); + + const { stdout } = await ng('test'); + + assert.match(stdout, /2 passed/, 'Expected 2 tests to pass.'); +} diff --git a/tests/e2e/tests/vitest/include.ts b/tests/e2e/tests/vitest/include.ts new file mode 100644 index 000000000000..4585194ef3c2 --- /dev/null +++ b/tests/e2e/tests/vitest/include.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { applyVitestBuilder } from '../../utils/vitest'; +import { ng } from '../../utils/process'; +import path from 'node:path'; + +export default async function (): Promise { + await applyVitestBuilder(); + + const { stdout: stdout1 } = await ng('test', '--include', path.resolve('src/app/app.spec.ts')); + assert.match(stdout1, /1 passed/, 'Expected 1 test to pass with absolute include.'); + + const { stdout: stdout2 } = await ng('test', '--include', path.normalize('src/app/app.spec.ts')); + assert.match(stdout2, /1 passed/, 'Expected 1 test to pass with relative include.'); +} diff --git a/tests/e2e/tests/vitest/larger-project-coverage.ts b/tests/e2e/tests/vitest/larger-project-coverage.ts new file mode 100644 index 000000000000..3594bdc7dfee --- /dev/null +++ b/tests/e2e/tests/vitest/larger-project-coverage.ts @@ -0,0 +1,111 @@ +import { ng } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; +import assert from 'node:assert'; +import { installPackage } from '../../utils/packages'; +import { updateJsonFile } from '../../utils/project'; +import { readFile } from '../../utils/fs'; + +export default async function () { + await applyVitestBuilder(); + await installPackage('@vitest/coverage-v8@4'); + + // Add coverage and threshold configuration to ensure coverage is calculated. + // Use the 'json' reporter to get a machine-readable output for assertions. + await updateJsonFile('angular.json', (json) => { + const project = Object.values(json['projects'])[0] as any; + const test = project['architect']['test']; + test.options = { + coverageReporters: ['json', 'text'], + coverageThresholds: { + // The generated component/service/pipe files are basic + // A threshold of 75 should be safe. + statements: 75, + }, + }; + }); + + const artifactCount = 100; + const initialTestCount = 1; + const generatedFiles = await generateArtifactsInBatches(artifactCount); + + const totalTests = initialTestCount + artifactCount; + const expectedMessage = new RegExp(`${totalTests} passed`); + const coverageJsonPath = 'coverage/test-project/coverage-final.json'; + + // Run tests in default (JSDOM) mode with coverage + const { stdout: jsdomStdout } = await ng('test', '--no-watch', '--coverage'); + assert.match(jsdomStdout, expectedMessage, `Expected ${totalTests} tests to pass in JSDOM mode.`); + + // Assert that every generated file is in the coverage report by reading the JSON output. + const jsdomSummary = JSON.parse(await readFile(coverageJsonPath)); + const jsdomSummaryKeys = Object.keys(jsdomSummary); + for (const file of generatedFiles) { + const found = jsdomSummaryKeys.some((key) => key.endsWith(file)); + assert.ok(found, `Expected ${file} to be in the JSDOM coverage report.`); + } + + // Setup for browser mode + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + + // Run tests in browser mode with coverage + const { stdout: browserStdout } = await ng( + 'test', + '--no-watch', + '--coverage', + '--browsers', + 'ChromiumHeadless', + ); + assert.match( + browserStdout, + expectedMessage, + `Expected ${totalTests} tests to pass in browser mode.`, + ); + + // Assert that every generated file is in the coverage report for browser mode. + const browserSummary = JSON.parse(await readFile(coverageJsonPath)); + const browserSummaryKeys = Object.keys(browserSummary); + for (const file of generatedFiles) { + const found = browserSummaryKeys.some((key) => key.endsWith(file)); + assert.ok(found, `Expected ${file} to be in the browser coverage report.`); + } +} + +async function generateArtifactsInBatches(artifactCount: number): Promise { + const BATCH_SIZE = 5; + const generatedFiles: string[] = []; + let commands: Promise[] = []; + + for (let i = 0; i < artifactCount; i++) { + const type = i % 3; + const name = `test-artifact-${i}`; + + let generateType: string; + let fileSuffix: string; + + switch (type) { + case 0: + generateType = 'component'; + fileSuffix = '.ts'; + break; + case 1: + generateType = 'service'; + fileSuffix = '.ts'; + break; + default: + generateType = 'pipe'; + fileSuffix = '-pipe.ts'; + break; + } + + commands.push(ng('generate', generateType, name, '--skip-tests=false')); + generatedFiles.push(`${name}${fileSuffix}`); + + if (commands.length === BATCH_SIZE || i === artifactCount - 1) { + await Promise.all(commands); + commands = []; + } + } + + return generatedFiles; +} diff --git a/tests/e2e/tests/vitest/larger-project.ts b/tests/e2e/tests/vitest/larger-project.ts new file mode 100644 index 000000000000..61b18b102c4b --- /dev/null +++ b/tests/e2e/tests/vitest/larger-project.ts @@ -0,0 +1,70 @@ +import { ng } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; +import assert from 'node:assert'; +import { installPackage } from '../../utils/packages'; +import { exec } from '../../utils/process'; + +export default async function () { + await applyVitestBuilder(); + + const artifactCount = 100; + // A new project starts with 1 test file (app.spec.ts) + // Each generated artifact will add one more test file. + const initialTestCount = 1; + + await generateArtifactsInBatches(artifactCount); + + const totalTests = initialTestCount + artifactCount; + const expectedMessage = new RegExp(`${totalTests} passed`); + + // Run tests in default (JSDOM) mode + const { stdout: jsdomStdout } = await ng('test', '--no-watch'); + assert.match(jsdomStdout, expectedMessage, `Expected ${totalTests} tests to pass in JSDOM mode.`); + + // Setup for browser mode + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + + // Run tests in browser mode + const { stdout: browserStdout } = await ng( + 'test', + '--no-watch', + '--browsers', + 'ChromiumHeadless', + ); + assert.match( + browserStdout, + expectedMessage, + `Expected ${totalTests} tests to pass in browser mode.`, + ); +} + +async function generateArtifactsInBatches(artifactCount: number): Promise { + const BATCH_SIZE = 5; + let commands: Promise[] = []; + + for (let i = 0; i < artifactCount; i++) { + const type = i % 3; + const name = `test-artifact-${i}`; + let generateType: string; + + switch (type) { + case 0: + generateType = 'component'; + break; + case 1: + generateType = 'service'; + break; + default: + generateType = 'pipe'; + break; + } + + commands.push(ng('generate', generateType, name, '--skip-tests=false')); + + if (commands.length === BATCH_SIZE || i === artifactCount - 1) { + await Promise.all(commands); + commands = []; + } + } +} diff --git a/tests/e2e/tests/vitest/runner-config-path.ts b/tests/e2e/tests/vitest/runner-config-path.ts new file mode 100644 index 000000000000..456469b2d6a4 --- /dev/null +++ b/tests/e2e/tests/vitest/runner-config-path.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { writeMultipleFiles } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; + +export default async function (): Promise { + await applyVitestBuilder(); + + // Create a custom Vitest configuration file. + const customConfigPath = 'vitest.custom.mjs'; + await writeMultipleFiles({ + [customConfigPath]: ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + // A unique option to confirm this file is being used. + passWithNoTests: true, + }, + }); + `, + }); + + const absoluteConfigPath = path.resolve(customConfigPath); + const { stdout } = await ng('test', `--runner-config=${absoluteConfigPath}`); + + // Assert that the CLI logs the use of the specified configuration file. + assert.match( + stdout, + /vitest\.custom\.mjs/, + 'Expected a message confirming the use of the custom config file.', + ); +} diff --git a/tests/e2e/tests/vitest/snapshot.ts b/tests/e2e/tests/vitest/snapshot.ts new file mode 100644 index 000000000000..f099ba6f8d30 --- /dev/null +++ b/tests/e2e/tests/vitest/snapshot.ts @@ -0,0 +1,79 @@ +import { ng } from '../../utils/process'; +import { replaceInFile, readFile, writeFile } from '../../utils/fs'; +import { applyVitestBuilder } from '../../utils/vitest'; +import assert from 'node:assert/strict'; +import { stripVTControlCharacters } from 'node:util'; + +export default async function () { + // Set up the test project to use the vitest runner + await applyVitestBuilder(); + + // Add snapshot assertions to the test file + await replaceInFile( + 'src/app/app.spec.ts', + `describe('App', () => {`, + ` +describe('App', () => { + it('should match file snapshot', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect((app as any).title()).toMatchSnapshot(); + }); + + it('should match inline snapshot', () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + expect((app as any).title()).toMatchInlineSnapshot(); + }); +`, + ); + + // Synchronize line endings for Vitest which currently may miscalculate line counts + // with mixed file line endings. + let content = await readFile('src/app/app.spec.ts'); + content = content.replace(/\r\n/g, '\n'); + content = content.replace(/\r/g, '\n'); + await writeFile('src/app/app.spec.ts', content); + + // First run: create snapshots + const { stdout: firstRunStdout } = await ng('test'); + assert.match( + stripVTControlCharacters(firstRunStdout), + /Snapshots\s+2 written/, + 'Snapshots were not written on the first run.', + ); + + const specContent = await readFile('src/app/app.spec.ts'); + assert.match( + specContent, + /toMatchInlineSnapshot\(`"test-project"`\)/, + 'Inline snapshot was not written to the spec file.', + ); + + const snapshotContent = await readFile('src/app/__snapshots__/app.spec.ts.snap'); + assert.match( + snapshotContent, + /exports\[`App > should match file snapshot 1`\] = `"test-project"`;/, + 'File snapshot was not written to disk.', + ); + + // Second run: tests should pass with existing snapshots + await ng('test'); + + // Modify component to break snapshots + await replaceInFile('src/app/app.ts', 'test-project', 'Snapshot is broken!'); + + // Third run: tests should fail with snapshot mismatch + await assert.rejects( + () => ng('test'), + (err: any) => { + assert.match( + stripVTControlCharacters(err.toString()), + /Snapshots\s+2 failed/, + 'Expected snapshot mismatch error, but a different error occurred.', + ); + return true; + }, + 'Snapshot mismatch did not cause the test to fail.', + ); +} diff --git a/tests/e2e/tests/vitest/tslib-resolution.ts b/tests/e2e/tests/vitest/tslib-resolution.ts new file mode 100644 index 000000000000..759d5e2b5728 --- /dev/null +++ b/tests/e2e/tests/vitest/tslib-resolution.ts @@ -0,0 +1,71 @@ +import { writeFile } from '../../utils/fs'; +import { installPackage } from '../../utils/packages'; +import { ng } from '../../utils/process'; +import { applyVitestBuilder } from '../../utils/vitest'; +import assert from 'node:assert'; + +export default async function () { + await applyVitestBuilder(); + await installPackage('playwright@1'); + await installPackage('@vitest/browser-playwright@4'); + + // Add a custom decorator to trigger tslib usage + await writeFile( + 'src/app/custom-decorator.ts', + ` + export function MyDecorator() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // do nothing + }; + } + `, + ); + + // Add a service that uses the decorator + await writeFile( + 'src/app/test.service.ts', + ` + import { Injectable } from '@angular/core'; + import { MyDecorator } from './custom-decorator'; + + @Injectable({ + providedIn: 'root' + }) + export class TestService { + @MyDecorator() + myMethod() { + return true; + } + } + `, + ); + + // Add a test for the service + await writeFile( + 'src/app/test.service.spec.ts', + ` + import { TestBed } from '@angular/core/testing'; + import { TestService } from './test.service'; + + describe('TestService', () => { + let service: TestService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(TestService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('myMethod should return true', () => { + expect(service.myMethod()).toBe(true); + }); + }); + `, + ); + + const { stdout } = await ng('test', '--no-watch', '--browsers', 'chromiumHeadless'); + assert.match(stdout, /2 passed/, 'Expected 2 tests to pass.'); +} diff --git a/tests/e2e/tests/web-test-runner/basic.ts b/tests/e2e/tests/web-test-runner/basic.ts new file mode 100644 index 000000000000..4985f872fb18 --- /dev/null +++ b/tests/e2e/tests/web-test-runner/basic.ts @@ -0,0 +1,15 @@ +import { noSilentNg } from '../../utils/process'; +import { applyWtrBuilder } from '../../utils/web-test-runner'; + +export default async function () { + // Temporary disabled due to failure. + return; + + await applyWtrBuilder(); + + const { stderr } = await noSilentNg('test'); + + if (!stderr.includes('Web Test Runner builder is currently EXPERIMENTAL')) { + throw new Error(`No experimental notice in stderr.\nSTDERR:\n\n${stderr}`); + } +} diff --git a/tests/e2e/utils/BUILD.bazel b/tests/e2e/utils/BUILD.bazel new file mode 100644 index 000000000000..8082ab9d97c4 --- /dev/null +++ b/tests/e2e/utils/BUILD.bazel @@ -0,0 +1,26 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "utils", + testonly = True, + srcs = glob(["**/*.ts"]), + data = [ + "//tests/e2e/ng-snapshot", + ], + deps = [ + "//:node_modules/@types/jasmine", + "//:node_modules/@types/node", + "//:node_modules/@types/semver", + "//:node_modules/fast-glob", + "//:node_modules/protractor", + "//:node_modules/puppeteer", + "//:node_modules/semver", + "//:node_modules/verdaccio", + "//:node_modules/verdaccio-auth-memory", + "//tests:node_modules/@types/tar-stream", + "//tests:node_modules/tar-stream", + "//tests:node_modules/tree-kill", + ], +) diff --git a/tests/e2e/utils/assets.ts b/tests/e2e/utils/assets.ts new file mode 100644 index 000000000000..153fdaccc6de --- /dev/null +++ b/tests/e2e/utils/assets.ts @@ -0,0 +1,69 @@ +import { join } from 'node:path'; +import { chmod } from 'node:fs/promises'; +import glob from 'fast-glob'; +import { getGlobalVariable } from './env'; +import { resolve } from 'node:path'; +import { copyFile } from './fs'; +import { installWorkspacePackages, setRegistry } from './packages'; +import { getTestProjectDir, useBuiltPackagesVersions } from './project'; + +export function assetDir(assetName: string) { + return join(__dirname, '../e2e/assets', assetName); +} + +export function copyProjectAsset(assetName: string, to?: string) { + const tempRoot = join(getGlobalVariable('projects-root'), 'test-project'); + const sourcePath = assetDir(assetName); + const targetPath = join(tempRoot, to || assetName); + + return copyFile(sourcePath, targetPath); +} + +export function copyAssets(assetName: string, to?: string) { + const seed = +Date.now(); + const tempRoot = join(getTestAssetsDir(), assetName + '-' + seed); + const root = assetDir(assetName); + + return Promise.resolve() + .then(() => { + const allFiles = glob.sync('**/*', { dot: true, cwd: root }); + + return allFiles.reduce((promise, filePath) => { + const toPath = + to !== undefined ? resolve(getTestProjectDir(), to, filePath) : join(tempRoot, filePath); + + return promise + .then(() => copyFile(join(root, filePath), toPath)) + .then(() => chmod(toPath, 0o777)); + }, Promise.resolve()); + }) + .then(() => tempRoot); +} + +/** + * @returns a method that once called will restore the environment + * to use the local NPM registry. + * */ +export async function createProjectFromAsset( + assetName: string, + useNpmPackages = false, + skipInstall = false, +): Promise<() => Promise> { + const dir = await copyAssets(assetName); + process.chdir(dir); + + await setRegistry(!useNpmPackages /** useTestRegistry */); + + if (!useNpmPackages) { + await useBuiltPackagesVersions(); + } + if (!skipInstall) { + await installWorkspacePackages(); + } + + return () => setRegistry(true /** useTestRegistry */); +} + +export function getTestAssetsDir(): string { + return join(getGlobalVariable('projects-root'), 'assets'); +} diff --git a/tests/e2e/utils/env.ts b/tests/e2e/utils/env.ts new file mode 100644 index 000000000000..d2f0feece0a7 --- /dev/null +++ b/tests/e2e/utils/env.ts @@ -0,0 +1,26 @@ +const ENV_PREFIX = 'LEGACY_CLI__'; + +export function setGlobalVariable(name: string, value: any) { + if (value === undefined) { + delete process.env[ENV_PREFIX + name]; + } else { + process.env[ENV_PREFIX + name] = JSON.stringify(value); + } +} + +export function getGlobalVariable(name: string): T { + const value = process.env[ENV_PREFIX + name]; + if (value === undefined) { + throw new Error(`Trying to access variable "${name}" but it's not defined.`); + } + return JSON.parse(value) as T; +} + +export function getGlobalVariablesEnv(): NodeJS.ProcessEnv { + return Object.keys(process.env) + .filter((v) => v.startsWith(ENV_PREFIX)) + .reduce((vars, n) => { + vars[n] = process.env[n]; + return vars; + }, {}); +} diff --git a/tests/e2e/utils/fs.ts b/tests/e2e/utils/fs.ts new file mode 100644 index 000000000000..2ebf7af9d28a --- /dev/null +++ b/tests/e2e/utils/fs.ts @@ -0,0 +1,134 @@ +import { promises as fs, constants } from 'node:fs'; +import { dirname, join } from 'node:path'; + +export function readFile(fileName: string): Promise { + return fs.readFile(fileName, 'utf-8'); +} + +export function writeFile(fileName: string, content: string, options?: any): Promise { + return fs.writeFile(fileName, content, options); +} + +export function deleteFile(path: string): Promise { + return fs.unlink(path); +} + +export function rimraf(path: string): Promise { + return fs.rm(path, { + force: true, + recursive: true, + maxRetries: 3, + }); +} + +export function moveFile(from: string, to: string): Promise { + return fs.rename(from, to); +} + +export function symlinkFile(from: string, to: string, type?: string): Promise { + return fs.symlink(from, to, type); +} + +export function createDir(path: string): Promise { + return fs.mkdir(path, { recursive: true }); +} + +export async function copyFile(from: string, to: string): Promise { + await createDir(dirname(to)); + + return fs.copyFile(from, to, constants.COPYFILE_FICLONE); +} + +export async function moveDirectory(from: string, to: string): Promise { + await rimraf(to); + await createDir(to); + + for (const entry of await fs.readdir(from)) { + const fromEntry = join(from, entry); + const toEntry = join(to, entry); + if ((await fs.stat(fromEntry)).isFile()) { + await copyFile(fromEntry, toEntry); + } else { + await moveDirectory(fromEntry, toEntry); + } + } +} + +export function writeMultipleFiles(fs: { [path: string]: string }) { + return Promise.all(Object.keys(fs).map((fileName) => writeFile(fileName, fs[fileName]))); +} + +export function replaceInFile(filePath: string, match: RegExp | string, replacement: string) { + return readFile(filePath).then((content: string) => + writeFile(filePath, content.replace(match, replacement)), + ); +} + +export function appendToFile(filePath: string, text: string, options?: any) { + return readFile(filePath).then((content: string) => + writeFile(filePath, content.concat(text), options), + ); +} + +export function prependToFile(filePath: string, text: string, options?: any) { + return readFile(filePath).then((content: string) => + writeFile(filePath, text.concat(content), options), + ); +} + +export async function expectFileMatchToExist(dir: string, regex: RegExp): Promise { + const files = await fs.readdir(dir); + const fileName = files.find((name) => regex.test(name)); + + if (!fileName) { + throw new Error(`File ${regex} was expected to exist but not found...`); + } + + return fileName; +} + +export async function expectFileNotToExist(fileName: string): Promise { + try { + await fs.access(fileName, constants.F_OK); + } catch { + return; + } + + throw new Error(`File ${fileName} was expected not to exist but found...`); +} + +export async function expectFileToExist(fileName: string): Promise { + try { + await fs.access(fileName, constants.F_OK); + } catch { + throw new Error(`File ${fileName} was expected to exist but not found...`); + } +} + +export async function expectFileToMatch(fileName: string, regEx: RegExp | string): Promise { + const content = await readFile(fileName); + + const found = typeof regEx === 'string' ? content.includes(regEx) : content.match(regEx); + + if (!found) { + throw new Error( + `File "${fileName}" did not contain "${regEx}"...\nContent:\n${content}\n------`, + ); + } +} + +export async function getFileSize(fileName: string) { + const stats = await fs.stat(fileName); + + return stats.size; +} + +export async function expectFileSizeToBeUnder(fileName: string, sizeInBytes: number) { + const fileSize = await getFileSize(fileName); + + if (fileSize > sizeInBytes) { + throw new Error( + `File "${fileName}" exceeded file size of "${sizeInBytes}". Size is ${fileSize}.`, + ); + } +} diff --git a/tests/e2e/utils/git.ts b/tests/e2e/utils/git.ts new file mode 100644 index 000000000000..39bb47ce7d52 --- /dev/null +++ b/tests/e2e/utils/git.ts @@ -0,0 +1,21 @@ +import { git, silentGit } from './process'; + +export async function gitClean(): Promise { + await silentGit('clean', '-df'); + await silentGit('reset', '--hard'); +} + +export async function expectGitToBeClean(): Promise { + const { stdout } = await silentGit('status', '--porcelain'); + if (stdout != '') { + throw new Error('Git repo is not clean...\n' + stdout); + } +} + +export async function gitCommit(message: string): Promise { + await git('add', '-A'); + const { stdout } = await silentGit('status', '--porcelain'); + if (stdout != '') { + await git('commit', '-am', message); + } +} diff --git a/tests/e2e/utils/jest.ts b/tests/e2e/utils/jest.ts new file mode 100644 index 000000000000..5dc1f0efe464 --- /dev/null +++ b/tests/e2e/utils/jest.ts @@ -0,0 +1,29 @@ +import { silentNpm } from './process'; +import { updateJsonFile } from './project'; + +/** Updates the `test` builder in the current workspace to use Jest with the given options. */ +export async function applyJestBuilder( + options: {} = { + polyfills: [], + tsConfig: 'tsconfig.spec.json', + }, +): Promise { + await silentNpm('install', 'jest@30.2.0', 'jest-environment-jsdom@30.2.0', '--save-dev'); + + await updateJsonFile('angular.json', (json) => { + const projects = Object.values(json['projects']); + if (projects.length !== 1) { + throw new Error( + `Expected exactly one project but found ${projects.length} projects named ${Object.keys( + json['projects'], + ).join(', ')}`, + ); + } + const project = projects[0]! as any; + + // Update to Jest builder. + const test = project['architect']['test']; + test['builder'] = '@angular-devkit/build-angular:jest'; + test['options'] = options; + }); +} diff --git a/tests/e2e/utils/network.ts b/tests/e2e/utils/network.ts new file mode 100644 index 000000000000..48602f0000d2 --- /dev/null +++ b/tests/e2e/utils/network.ts @@ -0,0 +1,30 @@ +import { createServer } from 'node:net'; + +/** + * Finds an available network port on the loopback interface (127.0.0.1). + * This is useful for tests that need to bind to a free port to avoid conflicts. + * Explicitly binds to IPv4 localhost to avoid firewall prompts, IPv6 binding issues, and ensure consistency. + * + * @returns A promise that resolves with an available port number. + */ +export function findFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = createServer(); + srv.once('listening', () => { + const address = srv.address(); + if (!address || typeof address === 'string') { + // Should not happen with TCP, but good for type safety + srv.close(() => reject(new Error('Failed to get server address'))); + return; + } + const port = address.port; + srv.close((e) => (e ? reject(e) : resolve(port))); + }); + + // If an error happens (e.g. during bind), the server is not listening, + // so we should not call close(). + srv.once('error', (e) => reject(e)); + // Explicitly listen on IPv4 localhost to avoid firewall prompts, IPv6 binding issues, and ensure consistency. + srv.listen(0, '127.0.0.1'); + }); +} diff --git a/tests/e2e/utils/packages.ts b/tests/e2e/utils/packages.ts new file mode 100644 index 000000000000..087d771f14b8 --- /dev/null +++ b/tests/e2e/utils/packages.ts @@ -0,0 +1,79 @@ +import { getGlobalVariable } from './env'; +import { ProcessOutput, silentBun, silentNpm, silentPnpm, silentYarn } from './process'; + +export interface PkgInfo { + readonly name: string; + readonly version: string; + readonly path: string; +} + +export function getActivePackageManager(): 'npm' | 'yarn' | 'bun' | 'pnpm' { + return getGlobalVariable('package-manager'); +} + +export async function installWorkspacePackages(options?: { force?: boolean }): Promise { + switch (getActivePackageManager()) { + case 'npm': + const npmArgs = ['install']; + if (options?.force) { + npmArgs.push('--force'); + } + await silentNpm(...npmArgs); + break; + case 'yarn': + await silentYarn('install'); + break; + case 'pnpm': + await silentPnpm('install'); + break; + case 'bun': + await silentBun('install'); + break; + } +} + +export function installPackage(specifier: string, registry?: string): Promise { + const registryOption = registry ? [`--registry=${registry}`] : []; + switch (getActivePackageManager()) { + case 'npm': + return silentNpm('install', specifier, ...registryOption); + case 'yarn': + return silentYarn('add', specifier, ...registryOption); + case 'bun': + return silentBun('add', specifier, ...registryOption); + case 'pnpm': + return silentPnpm('add', specifier, ...registryOption); + } +} + +export async function uninstallPackage(name: string): Promise { + try { + switch (getActivePackageManager()) { + case 'npm': + await silentNpm('uninstall', name); + break; + case 'yarn': + await silentYarn('remove', name); + break; + case 'bun': + await silentBun('remove', name); + break; + case 'pnpm': + await silentPnpm('remove', name); + break; + } + } catch (e) { + // Yarn throws an error when trying to remove a package that is not installed. + console.error(e); + } +} + +export async function setRegistry(useTestRegistry: boolean): Promise { + const url = useTestRegistry + ? getGlobalVariable('package-registry') + : 'https://registry.npmjs.org'; + + // Ensure local test registry is used when outside a project + // Yarn supports both `NPM_CONFIG_REGISTRY` and `YARN_REGISTRY`. + process.env['NPM_CONFIG_REGISTRY'] = url; +} diff --git a/tests/e2e/utils/process.ts b/tests/e2e/utils/process.ts new file mode 100644 index 000000000000..91216843086a --- /dev/null +++ b/tests/e2e/utils/process.ts @@ -0,0 +1,482 @@ +import { spawn, SpawnOptions } from 'node:child_process'; +import * as child_process from 'node:child_process'; +import { getGlobalVariable, getGlobalVariablesEnv } from './env'; +import treeKill from 'tree-kill'; +import { delimiter, join, resolve } from 'node:path'; +import { stripVTControlCharacters, styleText } from 'node:util'; + +interface ExecOptions { + silent?: boolean; + waitForMatch?: RegExp; + env?: NodeJS.ProcessEnv; + stdin?: string; + cwd?: string; +} + +/** + * While `NPM_CONFIG_` and `YARN_` are case insensitive we filter based on case. + * This is because when invoking a command using `yarn` it will add a bunch of these variables in lower case. + * This causes problems when we try to update the variables during the test setup. + */ +const NPM_CONFIG_RE = /^(NPM_CONFIG_|YARN_|NO_UPDATE_NOTIFIER)/; + +let _processes: child_process.ChildProcess[] = []; + +export type ProcessOutput = { + stdout: string; + stderr: string; +}; + +function _exec(options: ExecOptions, cmd: string, args: string[]): Promise { + const cwd = options.cwd ?? process.cwd(); + const env = options.env ?? process.env; + + console.log( + `==========================================================================================`, + ); + + // Ensure the custom npm and yarn global bin is on the PATH + // https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables + const paths = [ + join(getGlobalVariable('yarn-global'), 'bin'), + join(getGlobalVariable('npm-global'), process.platform.startsWith('win') ? '' : 'bin'), + env.PATH || process.env['PATH'], + ].join(delimiter); + + args = args.filter((x) => x !== undefined); + const flags = [ + options.silent && 'silent', + options.waitForMatch && `matching(${options.waitForMatch})`, + ] + .filter((x) => !!x) // Remove false and undefined. + .join(', ') + .replace(/^(.+)$/, ' [$1]'); // Proper formatting. + + console.log( + styleText(['blue'], `Running \`${cmd} ${args.map((x) => `"${x}"`).join(' ')}\`${flags}...`), + ); + console.log(styleText(['blue'], `CWD: ${cwd}`)); + + const spawnOptions: SpawnOptions = { + cwd, + env: { ...env, PATH: paths }, + }; + + if (process.platform.startsWith('win')) { + args.unshift('/c', cmd); + cmd = 'cmd.exe'; + spawnOptions['stdio'] = 'pipe'; + } + + const childProcess = child_process.spawn(cmd, args, spawnOptions); + + _processes.push(childProcess); + + // Create the error here so the stack shows who called this function. + const error = new Error(); + + const processPromise = new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + let matched = false; + + // Return log info about the current process status + function envDump() { + return `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`; + } + + childProcess.stdout!.on('data', (data: Buffer) => { + stdout += data.toString('utf-8'); + + if (options.waitForMatch && stdout.match(options.waitForMatch)) { + resolve({ stdout, stderr }); + matched = true; + } + + if (options.silent) { + return; + } + + data + .toString('utf-8') + .split(/[\n\r]+/) + .filter((line) => line !== '') + .forEach((line) => console.log(' ' + line)); + }); + + childProcess.stderr!.on('data', (data: Buffer) => { + stderr += data.toString('utf-8'); + + if (options.waitForMatch && stderr.match(options.waitForMatch)) { + resolve({ stdout, stderr }); + matched = true; + } + + if (options.silent) { + return; + } + + data + .toString('utf-8') + .split(/[\n\r]+/) + .filter((line) => line !== '') + .forEach((line) => console.error(styleText(['yellow'], ' ' + line))); + }); + + childProcess.on('close', (code) => { + _processes = _processes.filter((p) => p !== childProcess); + + if (options.waitForMatch && !matched) { + reject( + `Process output didn't match - "${cmd} ${args.join(' ')}": '${ + options.waitForMatch + }': ${code}...\n\n${envDump()}\n`, + ); + return; + } + + if (!code) { + resolve({ stdout, stderr }); + return; + } + + reject(`Process exit error - "${cmd} ${args.join(' ')}": ${code}...\n\n${envDump()}\n`); + }); + + childProcess.on('error', (err) => { + reject(`Process error - "${cmd} ${args.join(' ')}": ${err}...\n\n${envDump()}\n`); + }); + + // Provide input to stdin if given. + if (options.stdin) { + childProcess.stdin!.write(options.stdin); + childProcess.stdin!.end(); + } + }).catch((err) => { + error.message = err.toString(); + return Promise.reject(error); + }); + + if (!options.waitForMatch) { + return processPromise; + } + + let timeout: NodeJS.Timeout | undefined; + const timeoutPromise: Promise = new Promise((_resolve, reject) => { + // Wait for 60 seconds and timeout. + const duration = 60_000; + timeout = setTimeout(() => { + reject( + new Error(`Waiting for ${options.waitForMatch} timed out (timeout: ${duration}msec)...`), + ); + }, duration); + }); + + return Promise.race([timeoutPromise, processPromise]).finally( + () => timeout && clearTimeout(timeout), + ); +} + +export function extractNpmEnv() { + return Object.keys(process.env) + .filter((v) => NPM_CONFIG_RE.test(v)) + .reduce((vars, n) => { + vars[n] = process.env[n]; + return vars; + }, {}); +} + +export function extractCIAndInfraEnv(): NodeJS.ProcessEnv { + return Object.keys(process.env) + .filter( + (v) => + v.startsWith('SAUCE_') || + v === 'CI' || + v === 'CHROME_BIN' || + v === 'CHROME_PATH' || + v === 'CHROMEDRIVER_BIN' || + v.startsWith('JS_BINARY__'), + ) + .reduce((vars, n) => { + vars[n] = process.env[n]; + return vars; + }, {}); +} + +function extractNgEnv() { + return Object.keys(process.env) + .filter((v) => v.startsWith('NG_')) + .reduce((vars, n) => { + vars[n] = process.env[n]; + return vars; + }, {}); +} + +export async function waitForAnyProcessOutputToMatch( + match: RegExp, + timeout = 30000, +): Promise { + let timeoutId: ReturnType | null = null; + + // Race between _all_ processes, and the timeout. First one to resolve/reject wins. + const timeoutPromise: Promise = new Promise((_resolve, reject) => { + // Wait for 30 seconds and timeout. + timeoutId = setTimeout(() => { + reject(new Error(`Waiting for ${match} timed out (timeout: ${timeout}msec)...`)); + }, timeout); + }); + + const matchPromises: Promise[] = _processes.map( + (childProcess) => + new Promise((resolve) => { + let stdout = ''; + let stderr = ''; + + childProcess.stdout!.on('data', (data: Buffer) => { + stdout += data.toString(); + if (stripVTControlCharacters(stdout).match(match)) { + resolve({ stdout, stderr }); + } + }); + + childProcess.stderr!.on('data', (data: Buffer) => { + stderr += data.toString(); + if (stripVTControlCharacters(stderr).match(match)) { + resolve({ stdout, stderr }); + } + }); + }), + ); + + const matchingProcess = await Promise.race(matchPromises.concat([timeoutPromise])); + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + return matchingProcess; +} + +export async function killAllProcesses(signal = 'SIGTERM'): Promise { + const processesToKill: Promise[] = []; + + while (_processes.length) { + const childProc = _processes.pop(); + if (!childProc || childProc.pid === undefined) { + continue; + } + + processesToKill.push( + new Promise((resolve) => { + treeKill(childProc.pid!, signal, () => { + // Ignore all errors. + // This is due to a race condition with the `waitForMatch` logic. + // where promises are resolved on matches and not when the process terminates. + // Also in some cases in windows we get `The operation attempted is not supported`. + resolve(); + }); + }), + ); + } + + await Promise.all(processesToKill); +} + +export function exec(cmd: string, ...args: string[]) { + return _exec({}, cmd, args); +} + +export function silentExec(cmd: string, ...args: string[]) { + return _exec({ silent: true }, cmd, args); +} + +export function execWithEnv(cmd: string, args: string[], env: NodeJS.ProcessEnv, stdin?: string) { + return _exec({ env, stdin }, cmd, args); +} + +export async function execAndCaptureError( + cmd: string, + args: string[], + env?: NodeJS.ProcessEnv, + stdin?: string, +): Promise { + try { + await _exec({ env, stdin }, cmd, args); + throw new Error('Tried to capture subprocess exception, but it completed successfully.'); + } catch (err) { + if (err instanceof Error) { + return err; + } + throw new Error('Subprocess exception was not an Error instance'); + } +} + +export async function execAndWaitForOutputToMatch( + cmd: string, + args: string[], + match: RegExp, + env?: NodeJS.ProcessEnv, +) { + if (cmd === 'ng' && args[0] === 'serve') { + // Accept matches up to 20 times after the initial match. + // Useful because the Webpack watcher can rebuild a few times due to files changes that + // happened just before the build (e.g. `git clean`). + // This seems to be due to host file system differences, see + // https://nodejs.org/docs/latest/api/fs.html#fs_caveats + const maxRetries = 20; + let lastResult = await _exec({ waitForMatch: match, env }, cmd, args); + + for (let i = 0; i < maxRetries; i++) { + try { + lastResult = await waitForAnyProcessOutputToMatch(match, 2500); + } catch { + // If we timeout (no new match found), we assume the process is stable. + break; + } + } + + return lastResult; + } else { + return _exec({ waitForMatch: match, env }, cmd, args); + } +} + +export function ng(...args: string[]) { + const argv = getGlobalVariable('argv'); + const maybeSilentNg = argv['nosilent'] ? noSilentNg : silentNg; + if (['build', 'serve', 'test', 'e2e', 'extract-i18n'].indexOf(args[0]) != -1) { + if (args[0] == 'e2e') { + // Wait 1 second before running any end-to-end test. + return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => maybeSilentNg(...args)); + } + + return maybeSilentNg(...args); + } else { + return noSilentNg(...args); + } +} + +export function noSilentNg(...args: string[]) { + return _exec({}, 'ng', args); +} + +export function silentNg(...args: string[]) { + return _exec({ silent: true }, 'ng', args); +} + +export function silentNpm(...args: string[]): Promise; +export function silentNpm(args: string[], options?: { cwd?: string }): Promise; +export function silentNpm( + ...args: string[] | [args: string[], options?: { cwd?: string }] +): Promise { + if (Array.isArray(args[0])) { + const [params, options] = args; + return _exec( + { + silent: true, + cwd: (options as { cwd?: string } | undefined)?.cwd, + }, + 'npm', + params, + ); + } else { + return _exec({ silent: true }, 'npm', args as string[]); + } +} + +export function silentYarn(...args: string[]) { + return _exec({ silent: true }, 'yarn', args); +} + +export function silentPnpm(...args: string[]) { + return _exec({ silent: true }, 'pnpm', args); +} + +export function silentBun(...args: string[]) { + return _exec({ silent: true }, 'bun', args); +} + +export function globalNpm(args: string[], env?: NodeJS.ProcessEnv) { + if (!process.env.LEGACY_CLI_RUNNER) { + throw new Error( + 'The global npm cli should only be executed from the primary e2e runner process', + ); + } + + return _exec({ silent: true, env }, 'npm', args); +} + +export function node(...args: string[]) { + return _exec({}, process.execPath, args); +} + +export function git(...args: string[]) { + return _exec({}, process.env.GIT_BIN || 'git', args); +} + +export function silentGit(...args: string[]) { + return _exec({ silent: true }, process.env.GIT_BIN || 'git', args); +} + +/** + * Launch the given entry in an child process isolated to the test environment. + * + * The test environment includes the local NPM registry, isolated NPM globals, + * the PATH variable only referencing the local node_modules and local NPM + * registry (not the test runner or standard global node_modules). + */ +export async function launchTestProcess(entry: string, ...args: any[]): Promise { + // NOTE: do NOT use the bazel TEST_TMPDIR. When sandboxing is not enabled the + // TEST_TMPDIR is not sandboxed and has symlinks into the src dir in a + // parent directory. Symlinks into the src dir will include package.json, + // .git and other files/folders that may effect e2e tests. + + const tempRoot: string = getGlobalVariable('tmp-root'); + const TEMP = process.env.TEMP ?? process.env.TMPDIR ?? tempRoot; + + // Extract explicit environment variables for the test process. + const env: NodeJS.ProcessEnv = { + TEMP, + TMPDIR: TEMP, + HOME: TEMP, + + // Use BAZEL_TARGET as a metadata variable to show it is a + // process managed by bazel + BAZEL_TARGET: process.env.BAZEL_TARGET, + + ...extractNpmEnv(), + ...extractCIAndInfraEnv(), + ...extractNgEnv(), + ...getGlobalVariablesEnv(), + }; + + // Only include paths within the sandboxed test environment or external + // non angular-cli paths such as /usr/bin for generic commands. + env.PATH = process.env + .PATH!.split(delimiter) + .filter((p) => p.startsWith(tempRoot) || p.startsWith(TEMP) || !p.includes('angular-cli')) + .join(delimiter); + + const testProcessArgs = [ + // Note: `__dirname` is the bundle directory here. + resolve(__dirname, 'e2e/utils/test_process.js'), + entry, + ...args, + ]; + + return new Promise((resolve, reject) => { + spawn(process.execPath, testProcessArgs, { + stdio: 'inherit', + env, + }) + .on('close', (code) => { + if (!code) { + resolve(); + return; + } + + reject(`Process error - "${testProcessArgs}`); + }) + .on('error', (err) => { + reject(`Process exit error - "${testProcessArgs}]\n\n${err}`); + }); + }); +} diff --git a/tests/e2e/utils/project.ts b/tests/e2e/utils/project.ts new file mode 100644 index 000000000000..58e249d19895 --- /dev/null +++ b/tests/e2e/utils/project.ts @@ -0,0 +1,277 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { prerelease, SemVer } from 'semver'; +import { getGlobalVariable } from './env'; +import { readFile, replaceInFile, writeFile } from './fs'; +import { gitCommit } from './git'; +import { findFreePort } from './network'; +import { installWorkspacePackages, PkgInfo } from './packages'; +import { execAndWaitForOutputToMatch, git, ng } from './process'; +import { join } from 'node:path'; + +export function updateJsonFile(filePath: string, fn: (json: any) => any | void) { + return readFile(filePath).then((tsConfigJson) => { + // Remove single and multiline comments + const tsConfig = JSON.parse(tsConfigJson.replace(/\/\*\s(.|\n|\r)*\s\*\/|\/\/.*/g, '')) as any; + const result = fn(tsConfig) || tsConfig; + + return writeFile(filePath, JSON.stringify(result, null, 2)); + }); +} + +export function updateTsConfig(fn: (json: any) => any | void) { + return updateJsonFile('tsconfig.json', fn); +} + +export async function ngServe(...args: string[]) { + const port = await findFreePort(); + + const esbuild = getGlobalVariable('argv')['esbuild']; + const validBundleRegEx = esbuild ? /complete\./ : /Compiled successfully\./; + + await execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port', String(port), ...args], + validBundleRegEx, + ); + + return port; +} + +export async function prepareProjectForE2e(name: string) { + const argv: Record = getGlobalVariable('argv'); + + await git('config', 'user.email', 'angular-core+e2e@google.com'); + await git('config', 'user.name', 'Angular CLI E2E'); + await git('config', 'commit.gpgSign', 'false'); + await git('config', 'core.longpaths', 'true'); + + if (argv['ng-snapshots'] || argv['ng-tag']) { + await useSha(); + } + + console.log(`Project ${name} created... Installing packages.`); + await installWorkspacePackages(); + await ng('generate', 'private-e2e', '--related-app-name', name); + + await useCIChrome(name, 'e2e'); + await useCIChrome(name, ''); + await useCIDefaults(name); + + // Force sourcemaps to be from the root of the filesystem. + await updateJsonFile('tsconfig.json', (json) => { + json['compilerOptions']['sourceRoot'] = '/'; + }); + await gitCommit('prepare-project-for-e2e'); +} + +export function useBuiltPackagesVersions(): Promise { + const packages: { [name: string]: PkgInfo } = getGlobalVariable('package-tars'); + + return updateJsonFile('package.json', (json) => { + json['dependencies'] ??= {}; + json['devDependencies'] ??= {}; + + for (const packageName of Object.keys(packages)) { + if (packageName in json['dependencies']) { + json['dependencies'][packageName] = packages[packageName].version; + } else if (packageName in json['devDependencies']) { + json['devDependencies'][packageName] = packages[packageName].version; + } + } + }); +} + +export async function useSha(): Promise { + const argv = getGlobalVariable('argv'); + if (!argv['ng-snapshots'] && !argv['ng-tag']) { + return; + } + + // We need more than the sha here, version is also needed. Examples of latest tags: + // 7.0.0-beta.4+dd2a650 + // 6.1.6+4a8d56a + const label = argv['ng-tag'] || ''; + const ngSnapshotVersions = require('../ng-snapshot/package.json'); + + return updateJsonFile('package.json', (json) => { + // Install over the project with snapshot builds. + function replaceDependencies(key: string) { + const missingSnapshots: string[] = []; + Object.keys(json[key] || {}) + .filter((name) => name.startsWith('@angular/')) + .forEach((name) => { + const pkgName = name.split(/\//)[1]; + if (pkgName === 'cli' || pkgName === 'ssr' || pkgName === 'build') { + return; + } + + if (label) { + json[key][`@angular/${pkgName}`] = `github:angular/${pkgName}-builds${label}`; + } else { + const replacement = ngSnapshotVersions.dependencies[`@angular/${pkgName}`]; + if (!replacement) { + missingSnapshots.push(`missing @angular/${pkgName}`); + } + json[key][`@angular/${pkgName}`] = replacement; + } + }); + if (missingSnapshots.length > 0) { + throw new Error( + 'e2e test with --ng-snapshots requires all angular packages be ' + + 'listed in tests/e2e/ng-snapshot/package.json.\nErrors:\n' + + missingSnapshots.join('\n '), + ); + } + } + + replaceDependencies('dependencies'); + replaceDependencies('devDependencies'); + }); +} + +export function useCIDefaults(projectName = 'test-project'): Promise { + return updateJsonFile('angular.json', (workspaceJson) => { + // Disable progress reporting on CI to reduce spam. + const project = workspaceJson.projects[projectName]; + const appTargets = project.targets || project.architect; + appTargets.build.options.progress = false; + appTargets.test.options.progress = false; + if (appTargets.e2e) { + // Disable auto-updating webdriver in e2e. + appTargets.e2e.options.webdriverUpdate = false; + // Use a random port in e2e. + appTargets.e2e.options.port = 0; + } + + if (appTargets.serve) { + // Use a random port in serve. + appTargets.serve.options ??= {}; + appTargets.serve.options.port = 0; + } + }); +} + +export async function useCIChrome(projectName: string, projectDir = ''): Promise { + const protractorConf = path.join(projectDir, 'protractor.conf.js'); + if (fs.existsSync(protractorConf)) { + // Ensure the headless sandboxed chrome is configured in the protractor config + await replaceInFile( + protractorConf, + `browserName: 'chrome'`, + `browserName: 'chrome', + chromeOptions: { + args: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], + binary: String.raw\`${process.env.CHROME_BIN}\`, + }`, + ); + await replaceInFile( + protractorConf, + 'directConnect: true,', + `directConnect: true, chromeDriver: String.raw\`${process.env.CHROMEDRIVER_BIN}\`,`, + ); + } + + const karmaConf = path.join(projectDir, 'karma.conf.js'); + if (fs.existsSync(karmaConf)) { + // Ensure the headless sandboxed chrome is configured in the karma config + await replaceInFile( + karmaConf, + `browsers: ['Chrome'],`, + `browsers: ['ChromeHeadlessNoSandbox'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'], + }, + },`, + ); + } + + // Update to use the headless sandboxed chrome + return updateJsonFile('angular.json', (workspaceJson) => { + const project = workspaceJson.projects[projectName]; + const appTargets = project.targets || project.architect; + if (appTargets.test.builder === '@angular/build:unit-test') { + appTargets.test.options.browsers = ['ChromeHeadlessNoSandbox']; + } else { + appTargets.test.options.browsers = 'ChromeHeadlessNoSandbox'; + } + }); +} + +export function getNgCLIVersion(): SemVer { + const packages: { [name: string]: PkgInfo } = getGlobalVariable('package-tars'); + + return new SemVer(packages['@angular/cli'].version); +} + +export function isPrereleaseCli(): boolean { + return (prerelease(getNgCLIVersion())?.length ?? 0) > 0; +} + +export function updateServerFileForEsbuild(filepath: string): Promise { + return writeFile( + filepath, + ` + import { APP_BASE_HREF } from '@angular/common'; + import { CommonEngine } from '@angular/ssr/node'; + import express from 'express'; + import { join, resolve } from 'node:path'; + import bootstrap from './main.server'; + + // The Express app is exported so that it can be used by serverless Functions. + export function app(): express.Express { + const server = express(); + const serverDistFolder = import.meta.dirname; + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + server.use(express.static(browserDistFolder, { + maxAge: '1y', + index: false, + })); + + // All regular routes use the Angular engine + server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; + } + + function run(): void { + const port = process.env['PORT'] || 4000; + const server = app(); + server.listen(port, (error) => { + if (error) { + throw error; + } + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + run(); + `, + ); +} + +export function getTestProjectDir(): string { + return join(getGlobalVariable('projects-root'), 'test-project'); +} diff --git a/tests/e2e/utils/puppeteer.ts b/tests/e2e/utils/puppeteer.ts new file mode 100644 index 000000000000..8cab9f2ddef6 --- /dev/null +++ b/tests/e2e/utils/puppeteer.ts @@ -0,0 +1,90 @@ +import { type Page, launch } from 'puppeteer'; +import { execAndWaitForOutputToMatch, killAllProcesses } from './process'; +import { stripVTControlCharacters } from 'node:util'; + +export interface BrowserTestOptions { + project?: string; + configuration?: string; + baseUrl?: string; + checkFn?: (page: Page) => Promise; + expectedTitleText?: string; +} + +export async function executeBrowserTest(options: BrowserTestOptions = {}) { + let url = options.baseUrl; + let hasStartedServer = false; + + try { + if (!url) { + // Start serving and find address (1 - Webpack; 2 - Vite) + const match = /(?:open your browser on|Local:)\s+(http:\/\/localhost:\d+\/)/; + const serveArgs = ['serve', '--port=0']; + if (options.project) { + serveArgs.push(options.project); + } + if (options.configuration) { + serveArgs.push(`--configuration=${options.configuration}`); + } + + const { stdout } = await execAndWaitForOutputToMatch('ng', serveArgs, match, { + ...process.env, + 'NO_COLOR': '1', + }); + url = stripVTControlCharacters(stdout).match(match)?.[1]; + if (!url) { + throw new Error('Could not find serving URL'); + } + hasStartedServer = true; + } + + const browser = await launch({ + executablePath: process.env['CHROME_BIN'], + headless: true, + args: ['--no-sandbox'], + }); + try { + const page = await browser.newPage(); + + // Capture errors + const errors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + page.on('pageerror', (err) => { + errors.push(err.toString()); + }); + + await page.goto(url); + + if (options.checkFn) { + await options.checkFn(page); + } else { + // Default check: verify h1 content and no browser errors + const expectedText = options.expectedTitleText || 'Hello, test-project'; + + // Wait for the h1 element to appear and contain the expected text + await page.waitForFunction( + (selector: string, text: string) => { + const doc = (globalThis as any).document; + return doc.querySelector(selector)?.textContent?.includes(text); + }, + { timeout: 10000 }, // Max 10 seconds wait time + 'h1', + expectedText, + ); + } + + if (errors.length > 0) { + throw new Error(`Browser console errors detected:\n${errors.join('\n')}`); + } + } finally { + await browser.close(); + } + } finally { + if (hasStartedServer) { + await killAllProcesses(); + } + } +} diff --git a/tests/e2e/utils/registry.ts b/tests/e2e/utils/registry.ts new file mode 100644 index 000000000000..fd557c116120 --- /dev/null +++ b/tests/e2e/utils/registry.ts @@ -0,0 +1,117 @@ +import { ChildProcess, fork } from 'node:child_process'; +import { on } from 'node:events'; +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import { getGlobalVariable } from './env'; +import { writeFile, readFile } from './fs'; +import { existsSync } from 'node:fs'; + +export async function createNpmRegistry( + port: number, + httpsPort: number, + withAuthentication = false, +): Promise { + // Setup local package registry + const registryPath = join(getGlobalVariable('tmp-root'), 'registry'); + if (!existsSync(registryPath)) { + await mkdir(registryPath); + } + + const configFileName = withAuthentication ? 'verdaccio_auth.yaml' : 'verdaccio.yaml'; + let configContent = await readFile(join(__dirname, '../', configFileName)); + configContent = configContent + .replace(/\$\{HTTP_PORT\}/g, String(port)) + .replace(/\$\{HTTPS_PORT\}/g, String(httpsPort)); + const configPath = join(registryPath, configFileName); + + await writeFile(configPath, configContent); + + const verdaccioServer = fork(require.resolve('verdaccio/bin/verdaccio'), ['-c', configPath]); + for await (const events of on(verdaccioServer, 'message', { + signal: AbortSignal.timeout(30_000), + })) { + if ( + events.some( + (event: unknown) => + event && + typeof event === 'object' && + 'verdaccio_started' in event && + event.verdaccio_started, + ) + ) { + break; + } + } + + return verdaccioServer; +} + +// Token was generated using `echo -n 'testing:s3cret' | openssl base64`. +const VALID_TOKEN = `dGVzdGluZzpzM2NyZXQ=`; + +export async function createNpmConfigForAuthentication( + /** + * When true, the authentication token will be scoped to the registry URL. + * @example + * ```ini + * //localhost:4876/:_auth="dGVzdGluZzpzM2NyZXQ=" + * ``` + * + * When false, the authentication will be added as seperate key. + * @example + * ```ini + * _auth="dGVzdGluZzpzM2NyZXQ="` + * ``` + */ + scopedAuthentication: boolean, + /** When true, an incorrect token is used. Use this to validate authentication failures. */ + invalidToken = false, +): Promise { + const token = invalidToken ? `invalid=` : VALID_TOKEN; + const registry = (getGlobalVariable('package-secure-registry') as string).replace(/^\w+:/, ''); + + await writeFile( + '.npmrc', + scopedAuthentication + ? ` +${registry}/:_auth="${token}" +registry=http:${registry} +` + : ` +_auth="${token}" +registry=http:${registry} +`, + ); + + await writeFile( + '.yarnrc', + scopedAuthentication + ? ` +${registry}/:_auth "${token}" +registry http:${registry} +` + : ` +_auth "${token}" +registry http:${registry} +`, + ); +} + +export function setNpmEnvVarsForAuthentication( + /** When true, an incorrect token is used. Use this to validate authentication failures. */ + invalidToken = false, + /** When true, `YARN_REGISTRY` is used instead of `NPM_CONFIG_REGISTRY`. */ + useYarnEnvVariable = false, +): void { + delete process.env['YARN_REGISTRY']; + delete process.env['NPM_CONFIG_REGISTRY']; + + const registryKey = useYarnEnvVariable ? 'YARN_REGISTRY' : 'NPM_CONFIG_REGISTRY'; + process.env[registryKey] = getGlobalVariable('package-secure-registry'); + + process.env['NPM_CONFIG__AUTH'] = invalidToken ? `invalid=` : VALID_TOKEN; + + // Needed for verdaccio when used with yarn + // https://verdaccio.org/docs/en/cli-registry#yarn + process.env['NPM_CONFIG_ALWAYS_AUTH'] = 'true'; +} diff --git a/tests/e2e/utils/tar.ts b/tests/e2e/utils/tar.ts new file mode 100644 index 000000000000..d78f47762337 --- /dev/null +++ b/tests/e2e/utils/tar.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { createReadStream } from 'node:fs'; +import { normalize } from 'node:path'; +import { createGunzip } from 'node:zlib'; +import { extract } from 'tar-stream'; + +/** + * Extract and return the contents of a single file out of a tar file. + * + * @param tarball the tar file to extract from + * @param filePath the path of the file to extract + * @returns the Buffer of file or an error on fs/tar error or file not found + */ +export function extractFile(tarball: string, filePath: string): Promise { + const normalizedFilePath = normalize(filePath); + + return new Promise((resolve, reject) => { + const extractor = extract(); + + extractor.on('entry', (header, stream, next) => { + if (normalize(header.name) !== normalizedFilePath) { + stream.resume(); + next(); + + return; + } + + const chunks: Buffer[] = []; + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => { + resolve(Buffer.concat(chunks)); + next(); + }); + }); + + extractor.on('finish', () => reject(new Error(`'${filePath}' not found in '${tarball}'.`))); + + createReadStream(tarball).pipe(createGunzip()).pipe(extractor).on('error', reject); + }); +} diff --git a/tests/e2e/utils/test_process.ts b/tests/e2e/utils/test_process.ts new file mode 100644 index 000000000000..af6bd61af365 --- /dev/null +++ b/tests/e2e/utils/test_process.ts @@ -0,0 +1,23 @@ +import { killAllProcesses } from './process'; + +const testScript: string = process.argv[2]; +const testModule = require(testScript); +const testFunction: () => Promise | void = + typeof testModule == 'function' + ? testModule + : typeof testModule.default == 'function' + ? testModule.default + : () => { + throw new Error('Invalid test module.'); + }; + +(async () => { + try { + await testFunction(); + } catch (e) { + console.error('Test Process error', e); + process.exitCode = -1; + } finally { + killAllProcesses().finally(() => process.exit()); + } +})(); diff --git a/tests/e2e/utils/utils.ts b/tests/e2e/utils/utils.ts new file mode 100644 index 000000000000..54cfa2d70fa8 --- /dev/null +++ b/tests/e2e/utils/utils.ts @@ -0,0 +1,46 @@ +import assert from 'node:assert'; +import { mkdtemp, realpath, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +export function expectToFail(fn: () => Promise, errorMessage?: string): Promise { + return fn().then( + () => { + const functionSource = fn.name || (fn).source || fn.toString(); + const errorDetails = errorMessage ? `\n\tDetails:\n\t${errorMessage}` : ''; + throw new Error( + `Function ${functionSource} was expected to fail, but succeeded.${errorDetails}`, + ); + }, + (err) => { + return err instanceof Error ? err : new Error(err); + }, + ); +} + +export async function mktempd(prefix: string, tempRoot?: string): Promise { + return realpath(await mkdtemp(path.join(tempRoot ?? tmpdir(), prefix))); +} + +export async function mockHome(cb: (home: string) => Promise): Promise { + const tempHome = await mktempd('angular-cli-e2e-home-'); + + const oldHome = process.env.HOME; + process.env.HOME = tempHome; + + try { + await cb(tempHome); + } finally { + process.env.HOME = oldHome; + + await rm(tempHome, { recursive: true, force: true }); + } +} + +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/tests/e2e/utils/version.ts b/tests/e2e/utils/version.ts new file mode 100644 index 000000000000..34dc29720b91 --- /dev/null +++ b/tests/e2e/utils/version.ts @@ -0,0 +1,13 @@ +import * as fs from 'node:fs'; +import * as semver from 'semver'; + +export function readNgVersion(): string { + const packageJson: any = JSON.parse( + fs.readFileSync('./node_modules/@angular/core/package.json', 'utf8'), + ) as { version: string }; + return packageJson['version']; +} + +export function ngVersionMatches(range: string): boolean { + return semver.satisfies(readNgVersion(), range); +} diff --git a/tests/e2e/utils/vitest.ts b/tests/e2e/utils/vitest.ts new file mode 100644 index 000000000000..af40e66b18b1 --- /dev/null +++ b/tests/e2e/utils/vitest.ts @@ -0,0 +1,29 @@ +import { silentNpm } from './process'; +import { updateJsonFile } from './project'; + +/** Updates the `test` builder in the current workspace to use Vitest. */ +export async function applyVitestBuilder(): Promise { + // These deps matches the deps in `@schematics/angular` + await silentNpm('install', 'vitest@^4.0.8', 'jsdom@^27.1.0', '--save-dev'); + + await updateJsonFile('angular.json', (json) => { + const projects = Object.values(json['projects']); + if (projects.length !== 1) { + throw new Error( + `Expected exactly one project but found ${projects.length} projects named ${Object.keys( + json['projects'], + ).join(', ')}`, + ); + } + const project = projects[0]! as any; + + // Update to Vitest builder. + const test = project['architect']['test']; + test['builder'] = '@angular/build:unit-test'; + test['options'] = {}; + }); + + await updateJsonFile('tsconfig.spec.json', (tsconfig) => { + tsconfig['compilerOptions']['types'] = ['vitest/globals']; + }); +} diff --git a/tests/e2e/utils/web-test-runner.ts b/tests/e2e/utils/web-test-runner.ts new file mode 100644 index 000000000000..da66c623b76e --- /dev/null +++ b/tests/e2e/utils/web-test-runner.ts @@ -0,0 +1,23 @@ +import { silentNpm } from './process'; +import { updateJsonFile } from './project'; + +/** Updates the `test` builder in the current workspace to use Web Test Runner with the given options. */ +export async function applyWtrBuilder(): Promise { + await silentNpm('install', '@web/test-runner', '--save-dev'); + + await updateJsonFile('angular.json', (json) => { + const projects = Object.values(json['projects']); + if (projects.length !== 1) { + throw new Error( + `Expected exactly one project but found ${projects.length} projects named ${Object.keys( + json['projects'], + ).join(', ')}`, + ); + } + const project = projects[0]! as any; + + // Update to Web Test Runner builder. + const test = project['architect']['test']; + test['builder'] = '@angular-devkit/build-angular:web-test-runner'; + }); +} diff --git a/tests/e2e_runner.ts b/tests/e2e_runner.ts new file mode 100644 index 000000000000..c7a672161b7a --- /dev/null +++ b/tests/e2e_runner.ts @@ -0,0 +1,432 @@ +import { parseArgs, styleText } from 'node:util'; +import { createConsoleLogger } from '../packages/angular_devkit/core/node'; +import glob from 'fast-glob'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import { rm } from 'node:fs/promises'; +import { getGlobalVariable, setGlobalVariable } from './e2e/utils/env'; +import { gitClean } from './e2e/utils/git'; +import { createNpmRegistry } from './e2e/utils/registry'; +import { launchTestProcess } from './e2e/utils/process'; +import { delimiter, dirname, join } from 'node:path'; +import { findFreePort } from './e2e/utils/network'; +import { extractFile } from './e2e/utils/tar'; +import { realpathSync } from 'node:fs'; +import { PkgInfo } from './e2e/utils/packages'; +import { getTestProjectDir } from './e2e/utils/project'; +import { mktempd } from './e2e/utils/utils'; + +Error.stackTraceLimit = Infinity; + +// tslint:disable:no-global-tslint-disable no-console + +/** + * Here's a short description of those flags: + * --debug If a test fails, block the thread so the temporary directory isn't deleted. + * --noproject Skip creating a project or using one. + * --noglobal Skip linking your local @angular/cli directory. Can save a few seconds. + * --nosilent Never silence ng commands. + * --ng-tag=TAG Use a specific tag for build snapshots. Similar to ng-snapshots but point to a + * tag instead of using the latest `main`. + * --ng-snapshots Install angular snapshot builds in the test project. + * --glob Run tests matching this glob pattern (relative to tests/e2e/). + * --ignore Ignore tests matching this glob pattern. + * --nb-shards Total number of shards that this is part of. Default is 2 if --shard is + * passed in. + * --shard Index of this processes' shard. + * --package-manager Package manager to use. + * --package=path An npm package to be published before running tests + * + * If unnamed flags are passed in, the list of tests will be filtered to include only those passed. + */ +const parsed = parseArgs({ + allowPositionals: true, + options: { + 'debug': { type: 'boolean', default: !!process.env.BUILD_WORKSPACE_DIRECTORY }, + 'esbuild': { type: 'boolean' }, + 'glob': { type: 'string', default: 'tests/**/*.js' }, + 'ignore': { type: 'string', multiple: true }, + 'ng-snapshots': { type: 'boolean' }, + 'ng-tag': { type: 'string' }, + 'ng-version': { type: 'string' }, + 'noglobal': { type: 'boolean' }, + 'noproject': { type: 'boolean' }, + 'nosilent': { type: 'boolean' }, + 'package': { type: 'string', multiple: true, default: ['./dist/_*.tgz'] }, + 'package-manager': { type: 'string', default: 'npm' }, + 'verbose': { type: 'boolean' }, + + 'nb-shards': { type: 'string' }, + 'shard': { type: 'string' }, + }, +}); + +const argv = { + ...parsed.values, + _: parsed.positionals, + 'nb-shards': + parsed.values['nb-shards'] ?? + (Number(process.env.E2E_SHARD_TOTAL ?? 1) * Number(process.env.TEST_TOTAL_SHARDS ?? 1) || 1), + shard: + parsed.values.shard ?? + (process.env.E2E_SHARD_INDEX === undefined && process.env.TEST_SHARD_INDEX === undefined + ? undefined + : Number(process.env.E2E_SHARD_INDEX ?? 0) * Number(process.env.TEST_TOTAL_SHARDS ?? 1) + + Number(process.env.TEST_SHARD_INDEX ?? 0)), +}; + +// Indicate sharding support for Bazel. +if (process.env['TEST_SHARD_STATUS_FILE']) { + fs.writeFileSync(process.env['TEST_SHARD_STATUS_FILE'], '', 'utf8'); +} + +/** + * Set the error code of the process to 255. This is to ensure that if something forces node + * to exit without finishing properly, the error code will be 255. Right now that code is not used. + * + * - 1 When tests succeed we already call `process.exit(0)`, so this doesn't change any correct + * behaviour. + * + * One such case that would force node <= v6 to exit with code 0, is a Promise that doesn't resolve. + */ +process.exitCode = 255; + +/** + * Mark this process as the main e2e_runner + */ +process.env.LEGACY_CLI_RUNNER = '1'; + +/** + * Add external git toolchain onto PATH + */ +if (process.env.GIT_BIN) { + process.env.PATH = process.env.PATH! + delimiter + dirname(process.env.GIT_BIN!); +} + +/** + * Add external browser toolchains onto PATH + */ +if (process.env.CHROME_BIN) { + process.env.PATH = process.env.PATH! + delimiter + dirname(process.env.CHROME_BIN!); +} + +const logger = createConsoleLogger(argv.verbose, process.stdout, process.stderr, { + info: (s) => s, + debug: (s) => s, + warn: (s) => styleText(['bold', 'yellow'], s), + error: (s) => styleText(['bold', 'red'], s), + fatal: (s) => styleText(['bold', 'red'], s), +}); + +const logStack = [logger]; +function lastLogger() { + return logStack.at(-1)!; +} + +// Under bazel the compiled file (.js) and types (.d.ts) are available. +const SRC_FILE_EXT_RE = /\.js$/; +const testGlob = (process.env.TESTBRIDGE_TEST_ONLY ?? argv.glob).replace(/\.ts$/, '.js'); + +const e2eRoot = path.join(__dirname, 'e2e'); +const allSetups = glob.sync(`setup/**/*.js`, { cwd: e2eRoot }).sort(); +const allInitializers = glob.sync(`initialize/**/*.js`, { cwd: e2eRoot }).sort(); + +const allTests = glob + .sync(testGlob, { cwd: e2eRoot, ignore: argv.ignore }) + // Replace windows slashes. + .map((name) => name.replace(/\\/g, '/')) + .filter((name) => { + if (name.endsWith('/setup.js')) { + return false; + } + if (!SRC_FILE_EXT_RE.test(name)) { + return false; + } + + // The below is to exclude specific tests that are not intented to run for the current package manager. + // This is also important as without the trickery the tests that take the longest ex: update.ts (2.5mins) + // will be executed on the same shard. + const fileName = path.basename(name); + if ( + (fileName.startsWith('yarn-') && argv['package-manager'] !== 'yarn') || + (fileName.startsWith('npm-') && argv['package-manager'] !== 'npm') + ) { + return false; + } + + return true; + }) + .sort(); + +const shardId = argv['shard'] !== undefined ? Number(argv['shard']) : null; +const nbShards = shardId === null ? 1 : Number(argv['nb-shards']); +const tests = allTests.filter((name) => { + // Check for naming tests on command line. + if (argv._.length == 0) { + return true; + } + + return argv._.some((argName) => { + return ( + path.join(process.cwd(), argName + '') == path.join(__dirname, 'e2e', name) || + argName == name || + argName == name.replace(SRC_FILE_EXT_RE, '') + ); + }); +}); + +console.log(`Running with shard configuration:`); +console.log(`Total shards: ${nbShards}, current shard: ${shardId}`); + +// Remove tests that are not part of this shard. +const testsToRun = tests.filter((name, i) => shardId === null || i % nbShards == shardId); + +if (testsToRun.length === 0) { + if (shardId !== null && tests.length <= shardId) { + console.log(`No tests to run on shard ${shardId}, exiting`); + console.log(`Without sharding, there were ${tests.length} tests found.`); + process.exit(0); + } else { + console.log(`No tests would be ran, aborting.`); + process.exit(1); + } +} + +if (shardId !== null) { + console.log(`Running shard ${shardId} of ${nbShards}`); +} + +/** + * Load all the files from the e2e, filter and sort them and build a promise of their default + * export. + */ +if (testsToRun.length == allTests.length) { + console.log(`Running ${testsToRun.length} tests`); +} else { + console.log(`Running ${testsToRun.length} tests (${allTests.length} total)`); +} + +console.log(['Tests:', ...testsToRun].join('\n ')); + +setGlobalVariable('argv', argv); +setGlobalVariable('package-manager', argv['package-manager']); + +// Use the chrome supplied by bazel or the puppeteer chrome and webdriver-manager driver outside. +// This is needed by karma-chrome-launcher, protractor etc. +// https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer +// +// Resolve from relative paths to absolute paths within the bazel runfiles tree +// so subprocesses spawned in a different working directory can still find them. +process.env.CHROME_BIN = path.resolve(process.env.CHROME_BIN!); +process.env.CHROME_PATH = path.resolve(process.env.CHROME_PATH!); +process.env.CHROMEDRIVER_BIN = path.resolve(process.env.CHROMEDRIVER_BIN!); + +(async () => { + const tempRoot = await mktempd('angular-cli-e2e-', process.env.E2E_TEMP); + setGlobalVariable('tmp-root', tempRoot); + + process.on('SIGINT', deleteTemporaryRoot); + process.on('exit', deleteTemporaryRoot); + + const [httpPort, httpsPort, packageTars] = await Promise.all([ + findFreePort(), + findFreePort(), + findPackageTars(), + ]); + setGlobalVariable('package-registry', 'http://localhost:' + httpPort); + setGlobalVariable('package-secure-registry', 'http://localhost:' + httpsPort); + setGlobalVariable('package-tars', packageTars); + + // NPM registries for the lifetime of the test execution + const registryProcess = await createNpmRegistry(httpPort, httpPort); + const secureRegistryProcess = await createNpmRegistry(httpPort, httpsPort, true); + + try { + console.log(` Using "${tempRoot}" as temporary directory for a new project.`); + + await runSteps(runSetup, allSetups, 'setup'); + await runSteps(runInitializer, allInitializers, 'initializer'); + await runSteps(runTest, testsToRun, 'test'); + + if (shardId !== null) { + console.log(styleText(['green'], `Done shard ${shardId} of ${nbShards}.`)); + } else { + console.log(styleText(['green'], 'Done.')); + } + + process.exitCode = 0; + } catch (err) { + if (err instanceof Error) { + console.log('\n'); + console.error(styleText(['red'], err.message)); + if (err.stack) { + console.error(styleText(['red'], err.stack)); + } + } else { + console.error(styleText(['red'], String(err))); + } + + if (argv.debug) { + console.log(`Current Directory: ${process.cwd()}`); + console.log('Will loop forever while you debug... CTRL-C to quit.'); + + // Wait forever until user explicitly cancels. + await new Promise(() => {}); + } + + process.exitCode = 1; + } finally { + registryProcess.kill(); + secureRegistryProcess.kill(); + } +})().catch((err) => { + console.error(styleText(['red'], `Unkown Error: ${err}`)); + process.exitCode = 1; +}); + +async function runSteps( + run: (name: string) => Promise | void, + steps: string[], + type: 'setup' | 'test' | 'initializer', +) { + const capsType = type[0].toUpperCase() + type.slice(1); + + for (const [stepIndex, relativeName] of steps.entries()) { + // Make sure this is a windows compatible path. + let absoluteName = path.join(e2eRoot, relativeName).replace(SRC_FILE_EXT_RE, ''); + if (/^win/.test(process.platform)) { + absoluteName = absoluteName.replace(/\\/g, path.posix.sep); + } + + const name = relativeName.replace(SRC_FILE_EXT_RE, ''); + const start = Date.now(); + + printHeader(name, stepIndex, steps.length, type); + + // Run the test function with the current file on the logStack. + logStack.push(lastLogger().createChild(absoluteName)); + try { + await run(absoluteName); + } catch (e) { + console.log('\n'); + console.error(styleText(['red'], `${capsType} "${name}" failed...`)); + + throw e; + } finally { + logStack.pop(); + } + + console.log('----'); + printFooter(name, type, start); + } +} + +function runSetup(absoluteName: string): Promise { + const module = require(absoluteName); + + return (typeof module === 'function' ? module : module.default)(); +} + +/** + * Run a file from the projects root directory in a subprocess via launchTestProcess(). + */ +function runInitializer(absoluteName: string): Promise { + process.chdir(getGlobalVariable('projects-root')); + + return launchTestProcess(absoluteName); +} + +/** + * Run a file from the main 'test-project' directory in a subprocess via launchTestProcess(). + */ +async function runTest(absoluteName: string): Promise { + process.chdir(getTestProjectDir()); + + await launchTestProcess(absoluteName); + await cleanTestProject(); +} + +async function cleanTestProject() { + await gitClean(); + + const testProject = getTestProjectDir(); + + // Note: Dist directory is not cleared between tests, as `git clean` + // doesn't delete it. + await rm(join(testProject, 'dist/'), { recursive: true, force: true }); +} + +function printHeader( + testName: string, + testIndex: number, + count: number, + type: 'setup' | 'initializer' | 'test', +) { + const text = `(${testIndex + 1} of ${count})`; + const fullIndex = testIndex * nbShards + (shardId ?? 0) + 1; + const shard = + shardId === null || type !== 'test' + ? '' + : styleText( + ['yellow'], + ` [${shardId}:${nbShards}]` + styleText(['bold'], ` (${fullIndex}/${tests.length})`), + ); + console.log( + styleText(['green'], `Running ${type} "`) + + styleText(['bold', 'blue'], testName) + + styleText(['green'], '" ') + + styleText(['bold', 'white'], text) + + shard + + styleText(['green'], '...'), + ); +} + +function printFooter(testName: string, type: 'setup' | 'initializer' | 'test', startTime: number) { + const capsType = type[0].toUpperCase() + type.slice(1); + + // Round to hundredth of a second. + const t = Math.round((Date.now() - startTime) / 10) / 100; + console.log( + styleText(['green'], `${capsType} "`) + + styleText(['bold', 'blue'], testName) + + styleText(['green'], '" took ') + + styleText(['bold', 'blue'], t.toFixed(2)) + + styleText(['green'], 's...'), + ); + console.log(''); +} + +// Collect the packages passed as arguments and return as {package-name => pkg-path} +async function findPackageTars(): Promise<{ [pkg: string]: PkgInfo }> { + const pkgs: string[] = (getGlobalVariable('argv').package as string[]).flatMap((p) => + glob.sync(p), + ); + + const pkgJsons = await Promise.all( + pkgs.map((pkg) => realpathSync(pkg)).map((pkg) => extractFile(pkg, 'npm_package/package.json')), + ); + + return pkgs.reduce( + (all, pkg, i) => { + const json = pkgJsons[i].toString('utf8'); + const { name, version } = JSON.parse(json) as { name: string; version: string }; + if (!name) { + throw new Error(`Package ${pkg} - package.json name/version not found`); + } + + all[name] = { path: realpathSync(pkg), name, version }; + return all; + }, + {} as { [pkg: string]: PkgInfo }, + ); +} + +function deleteTemporaryRoot(): void { + try { + fs.rmSync(getGlobalVariable('tmp-root'), { + recursive: true, + force: true, + maxRetries: 3, + }); + } catch {} +} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/README.md b/tests/legacy-cli/e2e/assets/8.0-project/README.md deleted file mode 100644 index 2a085c145853..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# EightProject - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.6. - -## 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 directive|pipe|service|class|guard|interface|enum|module`. - -## 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/master/README.md). diff --git a/tests/legacy-cli/e2e/assets/8.0-project/angular.json b/tests/legacy-cli/e2e/assets/8.0-project/angular.json deleted file mode 100644 index f19ab692aa2d..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/angular.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "eight-project": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss" - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/eight-project", - "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.app.json", - "aot": false, - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "extractCss": true, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ] - } - } - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "eight-project:build" - }, - "configurations": { - "production": { - "browserTarget": "eight-project:build:production" - } - } - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "eight-project:build" - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "tsconfig.app.json", - "tsconfig.spec.json", - "e2e/tsconfig.json" - ], - "exclude": [ - "**/node_modules/**" - ] - } - }, - "e2e": { - "builder": "@angular-devkit/build-angular:protractor", - "options": { - "protractorConfig": "e2e/protractor.conf.js", - "devServerTarget": "eight-project:serve" - }, - "configurations": { - "production": { - "devServerTarget": "eight-project:serve:production" - } - } - } - } - }}, - "defaultProject": "eight-project" -} \ No newline at end of file diff --git a/tests/legacy-cli/e2e/assets/8.0-project/browserslist b/tests/legacy-cli/e2e/assets/8.0-project/browserslist deleted file mode 100644 index 80848532e47d..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/browserslist +++ /dev/null @@ -1,12 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -> 0.5% -last 2 versions -Firefox ESR -not dead -not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/tests/legacy-cli/e2e/assets/8.0-project/e2e/protractor.conf.js b/tests/legacy-cli/e2e/assets/8.0-project/e2e/protractor.conf.js deleted file mode 100644 index 7c798cfff07e..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/e2e/protractor.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-check -// Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -/** - * @type { import("protractor").Config } - */ -exports.config = { - allScriptsTimeout: 11000, - specs: [ - './src/**/*.e2e-spec.ts' - ], - capabilities: { - browserName: 'chrome' - }, - directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} - }, - onPrepare() { - require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.json') - }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -}; \ No newline at end of file diff --git a/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.e2e-spec.ts b/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.e2e-spec.ts deleted file mode 100644 index afe2417e8a9b..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AppPage } from './app.po'; -import { browser, logging } from 'protractor'; - -describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display welcome message', () => { - page.navigateTo(); - expect(page.getTitleText()).toEqual('Welcome to eight-project!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - } as logging.Entry)); - }); -}); diff --git a/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.po.ts b/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.po.ts deleted file mode 100644 index 5776aa9eb80d..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/e2e/src/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get(browser.baseUrl) as Promise; - } - - getTitleText() { - return element(by.css('app-root h1')).getText() as Promise; - } -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/e2e/tsconfig.json b/tests/legacy-cli/e2e/assets/8.0-project/e2e/tsconfig.json deleted file mode 100644 index 39b800f78961..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/e2e/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/e2e", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/karma.conf.js b/tests/legacy-cli/e2e/assets/8.0-project/karma.conf.js deleted file mode 100644 index 7830d5f888ef..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/karma.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -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-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, './coverage/eight-project'), - reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true - }); -}; diff --git a/tests/legacy-cli/e2e/assets/8.0-project/package.json b/tests/legacy-cli/e2e/assets/8.0-project/package.json deleted file mode 100644 index bbb621f7130c..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "eight-project", - "version": "0.0.0", - "scripts": { - "ng": "ng", - "start": "ng serve", - "build": "ng build", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e" - }, - "private": true, - "dependencies": { - "@angular/animations": "~8.0.3", - "@angular/common": "~8.0.3", - "@angular/compiler": "~8.0.3", - "@angular/core": "~8.0.3", - "@angular/forms": "~8.0.3", - "@angular/platform-browser": "~8.0.3", - "@angular/platform-browser-dynamic": "~8.0.3", - "@angular/router": "~8.0.3", - "rxjs": "~6.4.0", - "tslib": "^1.9.0", - "zone.js": "~0.9.1" - }, - "devDependencies": { - "@angular-devkit/build-angular": "~0.800.6", - "@angular/cli": "~8.0.6", - "@angular/compiler-cli": "~8.0.3", - "@angular/language-service": "~8.0.3", - "@types/node": "~8.9.4", - "@types/jasmine": "~3.3.8", - "@types/jasminewd2": "~2.0.3", - "codelyzer": "^5.0.0", - "jasmine-core": "~3.4.0", - "jasmine-spec-reporter": "~4.2.1", - "karma": "~4.1.0", - "karma-chrome-launcher": "~2.2.0", - "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~2.0.1", - "karma-jasmine-html-reporter": "^1.4.0", - "protractor": "~5.4.0", - "ts-node": "~7.0.0", - "tslint": "~5.15.0", - "typescript": "~3.4.3" - } -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app-routing.module.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app-routing.module.ts deleted file mode 100644 index d425c6f56b57..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app-routing.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - -const routes: Routes = []; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] -}) -export class AppRoutingModule { } diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.html b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.html deleted file mode 100644 index 0f3d9d8b9f8e..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.html +++ /dev/null @@ -1,21 +0,0 @@ - -
-

- Welcome to {{ title }}! -

- Angular Logo -
-

Here are some links to help you start:

-
- - diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.scss b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.spec.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.spec.ts deleted file mode 100644 index ba7327a810ee..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TestBed, async } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule - ], - declarations: [ - AppComponent - ], - }).compileComponents(); - })); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have as title 'eight-project'`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('eight-project'); - }); - - 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 eight-project!'); - }); -}); diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.ts deleted file mode 100644 index 6f0d72ab53df..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] -}) -export class AppComponent { - title = 'eight-project'; -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.module.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.module.ts deleted file mode 100644 index 2c3ba2995c85..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/app/app.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; - -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; - -@NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - AppRoutingModule - ], - providers: [], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.prod.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.prod.ts deleted file mode 100644 index 3612073bc31c..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -}; diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.ts deleted file mode 100644 index 7b4f817adb75..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/environments/environment.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false -}; - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/favicon.ico b/tests/legacy-cli/e2e/assets/8.0-project/src/favicon.ico deleted file mode 100644 index 8081c7ceaf2b..000000000000 Binary files a/tests/legacy-cli/e2e/assets/8.0-project/src/favicon.ico and /dev/null differ diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/index.html b/tests/legacy-cli/e2e/assets/8.0-project/src/index.html deleted file mode 100644 index 2cba33bcd2c2..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - EightProject - - - - - - - - - diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/main.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/main.ts deleted file mode 100644 index c7b673cf44b3..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; - -if (environment.production) { - enableProdMode(); -} - -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/polyfills.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/polyfills.ts deleted file mode 100644 index aa665d6b8740..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/polyfills.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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.io/guide/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/dist/zone'; // Included with Angular CLI. - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/tests/legacy-cli/e2e/assets/8.0-project/src/test.ts b/tests/legacy-cli/e2e/assets/8.0-project/src/test.ts deleted file mode 100644 index 16317897b1c5..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/src/test.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js/dist/zone-testing'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -declare const require: any; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.app.json b/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.app.json deleted file mode 100644 index 31f8397ac9f3..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.app.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/test.ts", - "src/**/*.spec.ts" - ] -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.json b/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.json deleted file mode 100644 index a4df54fff629..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "downlevelIteration": true, - "experimentalDecorators": true, - "module": "esnext", - "moduleResolution": "node", - "importHelpers": true, - "target": "es2015", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2018", - "dom" - ] - } -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.spec.json b/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.spec.json deleted file mode 100644 index 04463b7811d2..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/tsconfig.spec.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] - }, - "files": [ - "src/test.ts", - "src/polyfills.ts" - ], - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/tests/legacy-cli/e2e/assets/8.0-project/tslint.json b/tests/legacy-cli/e2e/assets/8.0-project/tslint.json deleted file mode 100644 index 6b7a0e83118e..000000000000 --- a/tests/legacy-cli/e2e/assets/8.0-project/tslint.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "extends": "tslint:recommended", - "rules": { - "array-type": false, - "arrow-parens": false, - "deprecation": { - "severity": "warning" - }, - "component-class-suffix": true, - "contextual-lifecycle": true, - "directive-class-suffix": true, - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ], - "interface-name": false, - "max-classes-per-file": false, - "max-line-length": [ - true, - 140 - ], - "member-access": false, - "member-ordering": [ - true, - { - "order": [ - "static-field", - "instance-field", - "static-method", - "instance-method" - ] - } - ], - "no-consecutive-blank-lines": false, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-empty": false, - "no-inferrable-types": [ - true, - "ignore-params" - ], - "no-non-null-assertion": true, - "no-redundant-jsdoc": true, - "no-switch-case-fall-through": true, - "no-use-before-declare": true, - "no-var-requires": false, - "object-literal-key-quotes": [ - true, - "as-needed" - ], - "object-literal-sort-keys": false, - "ordered-imports": false, - "quotemark": [ - true, - "single" - ], - "trailing-comma": false, - "no-conflicting-lifecycle": true, - "no-host-metadata-property": true, - "no-input-rename": true, - "no-inputs-metadata-property": true, - "no-output-native": true, - "no-output-on-prefix": true, - "no-output-rename": true, - "no-outputs-metadata-property": true, - "template-banana-in-box": true, - "template-no-negated-async": true, - "use-lifecycle-interface": true, - "use-pipe-transform-interface": true - }, - "rulesDirectory": [ - "codelyzer" - ] -} \ No newline at end of file diff --git a/tests/legacy-cli/e2e/assets/add-collection-peer-bad/index.js b/tests/legacy-cli/e2e/assets/add-collection-peer-bad/index.js deleted file mode 100644 index 867b3a4eb6fd..000000000000 --- a/tests/legacy-cli/e2e/assets/add-collection-peer-bad/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = (options) => tree => tree.create(options.name || 'empty-file-peer-bad', ''); diff --git a/tests/legacy-cli/e2e/assets/add-collection-peer-good/index.js b/tests/legacy-cli/e2e/assets/add-collection-peer-good/index.js deleted file mode 100644 index 4d5dbdca2f69..000000000000 --- a/tests/legacy-cli/e2e/assets/add-collection-peer-good/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = (options) => tree => tree.create(options.name || 'empty-file-peer-good', ''); diff --git a/tests/legacy-cli/e2e/assets/add-collection/index.js b/tests/legacy-cli/e2e/assets/add-collection/index.js deleted file mode 100644 index cf404a768650..000000000000 --- a/tests/legacy-cli/e2e/assets/add-collection/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = (options) => tree => tree.create(options.name || 'empty-file', ''); diff --git a/tests/legacy-cli/e2e/assets/nested-schematic-dependency/index.js b/tests/legacy-cli/e2e/assets/nested-schematic-dependency/index.js deleted file mode 100644 index cf404a768650..000000000000 --- a/tests/legacy-cli/e2e/assets/nested-schematic-dependency/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = (options) => tree => tree.create(options.name || 'empty-file', ''); diff --git a/tests/legacy-cli/e2e/assets/nested-schematic-main/index.js b/tests/legacy-cli/e2e/assets/nested-schematic-main/index.js deleted file mode 100644 index 9763f96fbb58..000000000000 --- a/tests/legacy-cli/e2e/assets/nested-schematic-main/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.default = (options) => require("@angular-devkit/schematics").externalSchematic('empty-app-nested', 'nested', {}); diff --git a/tests/legacy-cli/e2e/assets/protractor-saucelabs.conf.js b/tests/legacy-cli/e2e/assets/protractor-saucelabs.conf.js deleted file mode 100644 index 5813bf6e0b9c..000000000000 --- a/tests/legacy-cli/e2e/assets/protractor-saucelabs.conf.js +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-check -// 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 tunnelIdentifier = process.env['SAUCE_TUNNEL_IDENTIFIER']; - -/** - * @type { import("protractor").Config } - */ -exports.config = { - sauceUser: process.env['SAUCE_USERNAME'], - sauceKey: process.env['SAUCE_ACCESS_KEY'], - - allScriptsTimeout: 11000, - specs: ['./src/**/*.e2e-spec.ts'], - - multiCapabilities: [ - { - browserName: 'chrome', - platform: 'Windows 10', - version: '89.0', - tunnelIdentifier, - }, - { - browserName: 'firefox', - version: '86.0', - platform: 'Windows 10', - tunnelIdentifier, - }, - { - browserName: 'firefox', - version: '78.0', // Latest Firefox ESR version - platform: 'Windows 10', - tunnelIdentifier, - }, - { - browserName: 'safari', - platform: 'macOS 11.00', - version: '14', - tunnelIdentifier, - }, - { - browserName: 'safari', - platform: 'macOS 10.15', - version: '13.1', - tunnelIdentifier, - }, - { - browserName: 'internet explorer', - platform: 'Windows 8.1', - version: '11', - tunnelIdentifier, - }, - { - browserName: "MicrosoftEdge", - platform: 'Windows 10', - version: "88.0", - tunnelIdentifier, - }, - ], - - // Only allow one session at a time to prevent over saturation of Saucelabs sessions. - maxSessions: 1, - - SELENIUM_PROMISE_MANAGER: false, - baseUrl: 'http://localhost:2000/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {}, - }, - - onPrepare() { - // Fix for Safari 12 -- https://github.com/angular/protractor/issues/4964 - browser.resetUrl = 'about:blank'; - - require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.json'), - }); - jasmine.getEnv().addReporter(new SpecReporter({ - spec: { - displayStacktrace: StacktraceOption.PRETTY - } - })); - }, -}; diff --git a/tests/legacy-cli/e2e/assets/ssl/server.crt b/tests/legacy-cli/e2e/assets/ssl/server.crt deleted file mode 100644 index 6891c4c67573..000000000000 --- a/tests/legacy-cli/e2e/assets/ssl/server.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID5jCCAs6gAwIBAgIJAJOebwfGCm61MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV -BAYTAlVTMRAwDgYDVQQIEwdHZW9yZ2lhMRAwDgYDVQQHEwdBdGxhbnRhMRAwDgYD -VQQKEwdBbmd1bGFyMRAwDgYDVQQLEwdBbmd1bGFyMB4XDTE2MTAwNDAxMDAyMVoX -DTI2MTAwMjAxMDAyMVowVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0dlb3JnaWEx -EDAOBgNVBAcTB0F0bGFudGExEDAOBgNVBAoTB0FuZ3VsYXIxEDAOBgNVBAsTB0Fu -Z3VsYXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDT6Q4d1+mw81SC -4K1qLbsMn4O459XDiDDU/cGBiE0byqi6RpaB0MujCPn35xdeCf1mdDw929leEIRB -w/fCN3VwE+4ZDM7sF6SgoSDN8YT/OOush4tDu0djH110I+i1Bfg4m7gVkUnJLUCv -vMMOlD19LDqqaxdY3ojXx8gZJW9sNtUH2vCICwsZ7aNZp2NcCNKpU7LppP4IomCd -GfG501kY/UtELVgNGX+zuJwIiH/2AQZ+fsaDBBD0Azanck2M/aq5yVKMG8y/S5WP -7LMvZs8ZHPSG73QINogRTYW0EKx7nT87vmrHRtCc9u4coPdqOzQN9BigCYVkYrTv -xkOX9VDHAgMBAAGjgbgwgbUwHQYDVR0OBBYEFG4VV6/aNLx/qFIS9MhAWuyeV5OX -MIGFBgNVHSMEfjB8gBRuFVev2jS8f6hSEvTIQFrsnleTl6FZpFcwVTELMAkGA1UE -BhMCVVMxEDAOBgNVBAgTB0dlb3JnaWExEDAOBgNVBAcTB0F0bGFudGExEDAOBgNV -BAoTB0FuZ3VsYXIxEDAOBgNVBAsTB0FuZ3VsYXKCCQCTnm8HxgputTAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDO4jZT/oKVxaiWr+jV5TD+qwThl9zT -Uw/ZpFDkdbZdY/baCFaLCiJwkK9+puMOabLvm1VzcnHHWCoiUNbWpw8AOumLEnTv -ze/5OZXJ6XlA9kd9f3hDlN5zNB3S+Z2nKIrkPGfxQZ603QCbWaptip5dxgek6oDZ -YXVtnbOnPznRsG5jh07U49RO8CNebqZLzdRToLgObbqYlfRMcbUxCOHXjnB5wUlp -377Iivm4ldnCTvFOjEiDh+FByWL5xic7PjyJPZFMidiYTmsGilP9XTFC83CRZwz7 -vW+RCSlU6x8Uejz98BPmASoqCuCTUeOo+2pFelFhX9NwR/Sb6b7ybdPv ------END CERTIFICATE----- diff --git a/tests/legacy-cli/e2e/assets/ssl/server.key b/tests/legacy-cli/e2e/assets/ssl/server.key deleted file mode 100644 index e0e0af0f8da8..000000000000 --- a/tests/legacy-cli/e2e/assets/ssl/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0+kOHdfpsPNUguCtai27DJ+DuOfVw4gw1P3BgYhNG8qoukaW -gdDLowj59+cXXgn9ZnQ8PdvZXhCEQcP3wjd1cBPuGQzO7BekoKEgzfGE/zjrrIeL -Q7tHYx9ddCPotQX4OJu4FZFJyS1Ar7zDDpQ9fSw6qmsXWN6I18fIGSVvbDbVB9rw -iAsLGe2jWadjXAjSqVOy6aT+CKJgnRnxudNZGP1LRC1YDRl/s7icCIh/9gEGfn7G -gwQQ9AM2p3JNjP2quclSjBvMv0uVj+yzL2bPGRz0hu90CDaIEU2FtBCse50/O75q -x0bQnPbuHKD3ajs0DfQYoAmFZGK078ZDl/VQxwIDAQABAoIBAEl17kXcNo/4GqDw -QE2hoslCdwhfnhQVn1AG09ESriBnRcylccF4308aaoVM4CXicqzUuJl9IEJimWav -B7GVRinfTtfyP71KiPCCSvv5sPBFDDYYGugVAS9UjTIYzLAMbLs7CDq5zglmnZkO -Z9QjAZnl/kRbsZFGO8wJ3s0Q1Cp/ygZcvFU331K2jHXW7B4YXiFOH/lBQrjdz0Gy -WBjX4zIdNWnwarvxu46IS/0z1P1YOHM8+B1Uv54MG94A6szBdd/Vp0cQRs78t/Cu -BQ1Rnuk16Pi+ieC5K04yUgeuNusYW0PWLtPX1nKNp9z46bmD1NHKAxaoDFXr7qP3 -pZCaDMkCgYEA8mmTYrhXJTRIrOxoUwM1e3OZ0uOxVXJJ8HF6X8t+UO6dFxXB/JC9 -ZBc+94cZQapaKFOeMmd/j3L2CQIjChk5yKV/G3Io+raxIoAAKPCkMF4NQQVvvNkS -CAGl61Qa78DoF5Habumz0AC1R9P877kNTC0aPSt4lhPWgfotbZNNMlMCgYEA38nM -s4a0pZseXPkuOtPYX/3Ms3E+d70XKSFuIMCHCg79YGsQ8h/9apYcPyeYkpQ0a4gs -I3IUqMaXC2OyqWA5LU1BZv51mXb6zcb2pokZfpiSWk+7sy5XjkE9EmQxp3xHfV3c -EO/DxHfWNvtMjESMbhu0yVzM2O/Aa53Tl9lqAT0CgYEA1dXBuHyqCtyTG08zO78B -55Ny5rAJ1zkI9jvz2hr0o0nJcvqzcyruliNXXRxkcCNoglg4nXfk81JSrGGhLSBR -c6hhdoF+mqKboLZO7c5Q14WvpWK5TVoiaMOja/J2DHYbhecYS2yGPH7TargaUBDq -JP9IPRtitOhs+Z0Jg7ZDi5cCgYAMb7B6gY/kbBxh2k8hYchyfS41AqQQD2gMFxmB -pHFcs7yM8SY97l0s4S6sq8ykyKupFiYtyhcv0elu7pltJDXJOLPbv2RVpPEHInlu -g8vw5xWrAydRK9Adza5RKVRBFHz8kIy8PDbK4kX7RDfay6xqKgv/7LJNk/VDhb/O -fnyPmQKBgQDg/o8Ubf/gxA9Husnuld4DBu3wwFhkMlWqyO9QH3cKgojQ2JGSrfDz -xHhetmhionEyzg0JCaMSpzgIHY+8o/NAwc++OjNHEoYp3XWM9GTp81ROMz6b83jV -biVR9N0MhONdwF6vtzDCcJxNIUe2p4lTvLf/Xd9jaQDNXe35Gxsdyg== ------END RSA PRIVATE KEY----- diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.html b/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.html deleted file mode 100644 index 5a532db9308f..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ -
-

hello world

- lazy - -
diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.scss b/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.scss deleted file mode 100644 index 5cde7b922336..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - background-color: blue; -} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.ts deleted file mode 100644 index 82a4059565d3..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Component, ViewEncapsulation} from '@angular/core'; -import {MyInjectable} from './injectable'; - - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], - encapsulation: ViewEncapsulation.None -}) -export class AppComponent { - constructor(public inj: MyInjectable) { - console.log(inj); - } -} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.module.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.module.ts deleted file mode 100644 index a649a90cb41b..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/app.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule, Component } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { RouterModule } from '@angular/router'; -import { AppComponent } from './app.component'; - -@Component({ - selector: 'home-view', - template: 'home!' -}) -export class HomeView {} - - -@NgModule({ - declarations: [ - AppComponent, - HomeView - ], - imports: [ - BrowserModule, - RouterModule.forRoot([ - {path: 'lazy', loadChildren: () => import('./lazy.module').then(m => m.LazyModule)}, - {path: '', component: HomeView} - ]) - ], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/feature.module.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/feature.module.ts deleted file mode 100644 index f464ca028b05..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/feature.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {NgModule, Component} from '@angular/core'; -import {RouterModule} from '@angular/router'; - -@Component({ - selector: 'feature-component', - template: 'foo.html' -}) -export class FeatureComponent {} - -@NgModule({ - declarations: [ - FeatureComponent - ], - imports: [ - RouterModule.forChild([ - { path: '', component: FeatureComponent} - ]) - ] -}) -export class FeatureModule {} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/lazy-feature.module.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/lazy-feature.module.ts deleted file mode 100644 index b7d72eab327b..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/feature/lazy-feature.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {NgModule, Component} from '@angular/core'; -import {RouterModule} from '@angular/router'; - -@Component({ - selector: 'lazy-feature-comp', - template: 'lazy feature!' -}) -export class LazyFeatureComponent {} - -@NgModule({ - imports: [ - RouterModule.forChild([ - {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, - {path: 'feature', loadChildren: './feature.module#FeatureModule'} - ]) - ], - declarations: [LazyFeatureComponent] -}) -export class LazyFeatureModule {} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/injectable.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/injectable.ts deleted file mode 100644 index feb7ab0b76a8..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/injectable.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {Injectable, Inject, ViewContainerRef} from '@angular/core'; -import {DOCUMENT} from '@angular/common'; - - -@Injectable() -export class MyInjectable { - constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {} -} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/lazy.module.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/lazy.module.ts deleted file mode 100644 index b3ebda410c2b..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/lazy.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {NgModule, Component} from '@angular/core'; -import {RouterModule} from '@angular/router'; - -@Component({ - selector: 'lazy-comp', - template: 'lazy!' -}) -export class LazyComponent {} - -@NgModule({ - imports: [ - RouterModule.forChild([ - {path: '', component: LazyComponent, pathMatch: 'full'}, - {path: 'feature', loadChildren: () => import( './feature/feature.module').then(m => m.FeatureModule)}, - {path: 'lazy-feature', loadChildren: () => import( './feature/lazy-feature.module').then(m => m.LazyFeatureModule)}, - ]), - ], - declarations: [LazyComponent] -}) -export class LazyModule {} - -export class SecondModule {} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/app/main.ts b/tests/legacy-cli/e2e/assets/webpack/test-app/app/main.ts deleted file mode 100644 index a2a7a6c6b76c..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/app/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import 'core-js/proposals/reflect-metadata'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {AppModule} from './app.module'; - -platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/index.html b/tests/legacy-cli/e2e/assets/webpack/test-app/index.html deleted file mode 100644 index 89fb0893c35d..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - Document - - - - - - - - diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/package.json b/tests/legacy-cli/e2e/assets/webpack/test-app/package.json deleted file mode 100644 index 95f91bb566d9..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "test", - "license": "MIT", - "dependencies": { - "@angular/common": "^12.0.0-next", - "@angular/compiler": "^12.0.0-next", - "@angular/compiler-cli": "^12.0.0-next", - "@angular/core": "^12.0.0-next", - "@angular/platform-browser": "^12.0.0-next", - "@angular/platform-browser-dynamic": "^12.0.0-next", - "@angular/platform-server": "^12.0.0-next", - "@angular/router": "^12.0.0-next", - "@ngtools/webpack": "0.0.0", - "core-js": "^3.10.0", - "rxjs": "^6.6.7", - "zone.js": "^0.11.4" - }, - "devDependencies": { - "raw-loader": "^4.0.2", - "sass": "^1.32.8", - "sass-loader": "^11.0.1", - "typescript": "~4.2.3", - "webpack": "^5.27.0", - "webpack-cli": "^4.5.0" - } -} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json b/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json deleted file mode 100644 index 46d83664cba4..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "", - "module": "es2020", - "moduleResolution": "node", - "target": "es2015", - "noImplicitAny": false, - "sourceMap": true, - "mapRoot": "", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": [ - "es2017", - "dom" - ], - "outDir": "lib", - "skipLibCheck": true, - "rootDir": "." - }, - "angularCompilerOptions": { - "enableIvy": true, - "disableTypeScriptVersionCheck": true, - "genDir": "./app/ngfactory", - "entryModule": "app/app.module#AppModule" - } -} diff --git a/tests/legacy-cli/e2e/assets/webpack/test-app/webpack.config.js b/tests/legacy-cli/e2e/assets/webpack/test-app/webpack.config.js deleted file mode 100644 index c5188e27fe88..000000000000 --- a/tests/legacy-cli/e2e/assets/webpack/test-app/webpack.config.js +++ /dev/null @@ -1,27 +0,0 @@ -const ngToolsWebpack = require('@ngtools/webpack'); -const path = require('path'); - -module.exports = { - resolve: { - extensions: ['.ts', '.js'] - }, - entry: './app/main.ts', - output: { - path: path.resolve('./dist'), - publicPath: 'dist/', - filename: 'app.main.js' - }, - plugins: [ - new ngToolsWebpack.ivy.AngularWebpackPlugin(), - ], - module: { - rules: [ - { test: /\.scss$/, use: ['raw-loader', 'sass-loader'] }, - { test: /\.html$/, loader: 'raw-loader' }, - { test: /\.ts$/, loader: ngToolsWebpack.ivy.AngularWebpackLoaderPath } - ] - }, - devServer: { - historyApiFallback: true - } -}; diff --git a/tests/legacy-cli/e2e/ng-snapshot/package.json b/tests/legacy-cli/e2e/ng-snapshot/package.json deleted file mode 100644 index d2ca01d84536..000000000000 --- a/tests/legacy-cli/e2e/ng-snapshot/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "description": "snapshot versions of Angular for e2e testing", - "private": true, - "dependencies": { - "@angular/animations": "github:angular/animations-builds#a64314e26d5df8d773afc58639936128c2a858e5", - "@angular/cdk": "github:angular/cdk-builds#fd416584a94de24744ce2daa579a746f8f8f7097", - "@angular/common": "github:angular/common-builds#bf9da482674c5dd6bbd4a0e13d1893bd0713a22b", - "@angular/compiler": "github:angular/compiler-builds#8d852abaa037f0c885efb1c59167e9da4f3f8e18", - "@angular/compiler-cli": "github:angular/compiler-cli-builds#dfa5be1380f350048971b401dceb881b27d818b4", - "@angular/core": "github:angular/core-builds#e5a08f3e33998992d22d20ddf0100fd01b0b6733", - "@angular/forms": "github:angular/forms-builds#2b10422d71164691e05f703c561a6dbd4148efb9", - "@angular/language-service": "github:angular/language-service-builds#1ecfc4a7c5681556aec2d6ff448ba4612866315b", - "@angular/localize": "github:angular/localize-builds#b70f072926a12e94565b43735845cd8649d3ec43", - "@angular/material": "github:angular/material2-builds#559f1608ec2cb991c60cbabb3047360f3ecc4608", - "@angular/material-moment-adapter": "github:angular/material-moment-adapter-builds#6862077c6a42467ab31e5208a0dd843af630f5e5", - "@angular/platform-browser": "github:angular/platform-browser-builds#d7890645485a1b4808e877f37286cc3722e8edf3", - "@angular/platform-browser-dynamic": "github:angular/platform-browser-dynamic-builds#832765ea30ed9093faf45b7a547e56751cfa73a0", - "@angular/platform-server": "github:angular/platform-server-builds#63e76757be997de7ce73706d3b0fc13dc89c6d65", - "@angular/router": "github:angular/router-builds#e50b807477a8a50e7fc9218b7c4b5aadf33e7035", - "@angular/service-worker": "github:angular/service-worker-builds#2094c898a08a927a17d0c701c8830ed4fd9549b3" - } -} diff --git a/tests/legacy-cli/e2e/setup/010-local-publish.ts b/tests/legacy-cli/e2e/setup/010-local-publish.ts deleted file mode 100644 index d6a761a2b60d..000000000000 --- a/tests/legacy-cli/e2e/setup/010-local-publish.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getGlobalVariable } from '../utils/env'; -import { npm } from '../utils/process'; -import { isPrereleaseCli } from '../utils/project'; - -export default async function () { - const testRegistry = getGlobalVariable('package-registry'); - await npm( - 'run', - 'admin', - '--', - 'publish', - '--no-versionCheck', - '--no-branchCheck', - `--registry=${testRegistry}`, - '--tag', - isPrereleaseCli() ? 'next' : 'latest', - ); -} diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts deleted file mode 100644 index 8c7a1d151b05..000000000000 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getGlobalVariable } from '../utils/env'; -import { exec, silentNpm } from '../utils/process'; - -export default async function() { - const argv = getGlobalVariable('argv'); - if (argv.noglobal) { - return; - } - - const testRegistry = getGlobalVariable('package-registry'); - - // Install global Angular CLI. - await silentNpm( - 'install', - '--global', - '@angular/cli', - `--registry=${testRegistry}`, - ); - - try { - await exec(process.platform.startsWith('win') ? 'where' : 'which', 'ng'); - } catch {} -} diff --git a/tests/legacy-cli/e2e/setup/200-create-tmp-dir.ts b/tests/legacy-cli/e2e/setup/200-create-tmp-dir.ts deleted file mode 100644 index ae4b16ebec6d..000000000000 --- a/tests/legacy-cli/e2e/setup/200-create-tmp-dir.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { mkdtempSync, realpathSync } from 'fs'; -import { tmpdir } from 'os'; -import { dirname, join } from 'path'; -import { getGlobalVariable, setGlobalVariable } from '../utils/env'; - -export default function() { - const argv = getGlobalVariable('argv'); - - // Get to a temporary directory. - let tempRoot: string; - if (argv.reuse) { - tempRoot = dirname(argv.reuse); - } else if (argv.tmpdir) { - tempRoot = argv.tmpdir; - } else { - tempRoot = mkdtempSync(join(realpathSync(tmpdir()), 'angular-cli-e2e-')); - } - console.log(` Using "${tempRoot}" as temporary directory for a new project.`); - setGlobalVariable('tmp-root', tempRoot); - process.chdir(tempRoot); -} diff --git a/tests/legacy-cli/e2e/setup/300-log-environment.ts b/tests/legacy-cli/e2e/setup/300-log-environment.ts deleted file mode 100644 index ab60477ba4ca..000000000000 --- a/tests/legacy-cli/e2e/setup/300-log-environment.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ng, node, npm } from '../utils/process'; - -export default async function() { - console.log('Environment:'); - - Object.keys(process.env).forEach(envName => { - // CI Logs should not contain environment variables that are considered secret - const lowerName = envName.toLowerCase(); - if (lowerName.includes('key') || lowerName.includes('secret')) { - return; - } - - console.log(` ${envName}: ${process.env[envName].replace(/[\n\r]+/g, '\n ')}`); - }); - - await node('--version'); - await npm('--version'); - await ng('version'); -} diff --git a/tests/legacy-cli/e2e/setup/500-create-project.ts b/tests/legacy-cli/e2e/setup/500-create-project.ts deleted file mode 100644 index 7b0649a66548..000000000000 --- a/tests/legacy-cli/e2e/setup/500-create-project.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { join } from 'path'; -import { getGlobalVariable } from '../utils/env'; -import { expectFileToExist, writeFile } from '../utils/fs'; -import { gitClean } from '../utils/git'; -import { setRegistry as setNPMConfigRegistry } from '../utils/packages'; -import { ng, npm } from '../utils/process'; -import { prepareProjectForE2e, updateJsonFile } from '../utils/project'; - -export default async function() { - const argv = getGlobalVariable('argv'); - - if (argv.noproject) { - return; - } - - if (argv.reuse) { - process.chdir(argv.reuse); - await gitClean(); - } else { - const extraArgs = []; - const testRegistry = getGlobalVariable('package-registry'); - const isCI = getGlobalVariable('ci'); - - // Ensure local test registry is used when outside a project - await setNPMConfigRegistry(true); - - await ng('new', 'test-project', '--skip-install', ...extraArgs); - await expectFileToExist(join(process.cwd(), 'test-project')); - process.chdir('./test-project'); - - // Disable the TS version check to make TS updates easier. - // Only VE does it, but on Ivy the i18n extraction uses VE. - await updateJsonFile('tsconfig.json', config => { - if (!config.angularCompilerOptions) { - config.angularCompilerOptions = {}; - } - config.angularCompilerOptions.disableTypeScriptVersionCheck = true; - }); - - // If on CI, the user configuration set above will handle project usage - if (!isCI) { - // Ensure local test registry is used inside a project - await writeFile('.npmrc', `registry=${testRegistry}`); - } - } - - await prepareProjectForE2e('test-project'); - await ng('version'); -} diff --git a/tests/legacy-cli/e2e/tests/basic/aot.ts b/tests/legacy-cli/e2e/tests/basic/aot.ts deleted file mode 100644 index 58b12b2352f9..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/aot.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - await ng('build', '--aot=true', '--configuration=development'); - await expectFileToMatch('dist/test-project/main.js', - /platformBrowser.*bootstrapModule.*AppModule/); -} diff --git a/tests/legacy-cli/e2e/tests/basic/build.ts b/tests/legacy-cli/e2e/tests/basic/build.ts deleted file mode 100644 index 5098588660f4..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/build.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expectFileToMatch, replaceInFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default async function() { - // Development build - await ng('build', '--configuration=development'); - await expectFileToMatch('dist/test-project/index.html', 'main.js'); - - // Named Development build - await ng('build', 'test-project', '--configuration=development'); - await ng('build', '--configuration=development', 'test-project', '--no-progress'); - await ng('build', '--configuration=development', '--no-progress', 'test-project'); - - // Enable Differential loading to run both size checks - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - // Production build - const { stderr: stderrProgress, stdout } = await ng('build', '--progress'); - await expectFileToMatch('dist/test-project/index.html', /main-es5\.[a-zA-Z0-9]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /main-es2017\.[a-zA-Z0-9]{20}\.js/); - - if (!stdout.includes('Initial ES5 Total')) { - throw new Error(`Expected stdout not to contain 'Initial ES5 Total' but it did.\n${stdout}`); - } - - if (!stdout.includes('Initial ES2017 Total')) { - throw new Error(`Expected stdout not to contain 'Initial ES2017 Total' but it did.\n${stdout}`); - } - - const logs: string[] = [ - 'Browser application bundle generation complete', - 'ES5 bundle generation complete', - 'Copying assets complete', - 'Index html generation complete', - ]; - - for (const log of logs) { - if (!stderrProgress.includes(log)) { - throw new Error(`Expected stderr to contain '${log}' but didn't.\n${stderrProgress}`); - } - } - - const { stderr: stderrNoProgress } = await ng('build', '--no-progress'); - for (const log of logs) { - if (stderrNoProgress.includes(log)) { - throw new Error(`Expected stderr not to contain '${log}' but it did.\n${stderrProgress}`); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/basic/e2e.ts b/tests/legacy-cli/e2e/tests/basic/e2e.ts deleted file mode 100644 index d547320908f6..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/e2e.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - ng, - execAndWaitForOutputToMatch, - killAllProcesses -} from '../../utils/process'; -import {expectToFail} from '../../utils/utils'; -import {moveFile, copyFile, replaceInFile} from '../../utils/fs'; - -export default function () { - return Promise.resolve() - // Should fail without serving - .then(() => expectToFail(() => ng('e2e', 'test-project', '--devServerTarget='))) - // These should work. - .then(() => ng('e2e', 'test-project')) - .then(() => ng('e2e', 'test-project', '--devServerTarget=test-project:serve')) - // Should accept different config file - .then(() => moveFile('./e2e/protractor.conf.js', - './e2e/renamed-protractor.conf.js')) - .then(() => ng('e2e', 'test-project', - '--protractorConfig=e2e/renamed-protractor.conf.js')) - .then(() => moveFile('./e2e/renamed-protractor.conf.js', './e2e/protractor.conf.js')) - // Should accept different multiple spec files - .then(() => moveFile('./e2e/src/app.e2e-spec.ts', - './e2e/src/renamed-app.e2e-spec.ts')) - .then(() => copyFile('./e2e/src/renamed-app.e2e-spec.ts', - './e2e/src/another-app.e2e-spec.ts')) - .then(() => ng('e2e', 'test-project', '--specs', './e2e/renamed-app.e2e-spec.ts', - '--specs', './e2e/another-app.e2e-spec.ts')) - // Rename the spec back to how it was. - .then(() => moveFile('./e2e/src/renamed-app.e2e-spec.ts', - './e2e/src/app.e2e-spec.ts')) - // Suites block need to be added in the protractor.conf.js file to test suites - .then(() => replaceInFile('e2e/protractor.conf.js', `allScriptsTimeout: 11000,`, - `allScriptsTimeout: 11000, - suites: { - app: './e2e/src/app.e2e-spec.ts' - }, - `)) - .then(() => ng('e2e', 'test-project', '--suite=app')) - // Remove suites block from protractor.conf.js file after testing suites - .then(() => replaceInFile('e2e/protractor.conf.js', `allScriptsTimeout: 11000, - suites: { - app: './e2e/src/app.e2e-spec.ts' - }, - `, `allScriptsTimeout: 11000,` - )) - // Should run side-by-side with `ng serve` - .then(() => execAndWaitForOutputToMatch('ng', ['serve'], - / Compiled successfully./)) - .then(() => ng('e2e', 'test-project', '--devServerTarget=')) - // Should fail without updated webdriver - .then(() => replaceInFile('e2e/protractor.conf.js', /chromeDriver: String.raw`[^`]*`,/, '')) - .then(() => expectToFail(() => ng('e2e', 'test-project', '--no-webdriver-update', '--devServerTarget='))) - .then(() => killAllProcesses(), (err) => { - killAllProcesses(); - throw err; - }); -} diff --git a/tests/legacy-cli/e2e/tests/basic/environment.ts b/tests/legacy-cli/e2e/tests/basic/environment.ts deleted file mode 100644 index 239af2d9f60a..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/environment.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - - -export default async function () { - // Try a prod build. - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.build.configurations['prod-env'] = { - ...appArchitect.build.configurations['development'], - fileReplacements: [ - { - src: 'src/environments/environment.ts', - replaceWith: 'src/environments/environment.prod.ts', - }, - ], - }; - }); - - await ng('build', '--configuration=prod-env'); - await expectFileToMatch('dist/test-project/main.js', /production:\s*true/); -} diff --git a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts b/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts deleted file mode 100644 index 5bf34c937a61..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as os from 'os'; -import { join } from 'path'; -import { writeFile, deleteFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - - -export default function() { - const homedir = os.homedir(); - const globalConfigPath = join(homedir, '.angular-config.json'); - return Promise.resolve() - .then(() => writeFile(globalConfigPath, '{"version":1}')) - .then(() => process.chdir(homedir)) - .then(() => ng('new', 'proj-name', '--dry-run')) - .then(() => deleteFile(globalConfigPath)) - // Test that we cannot create a project inside another project. - .then(() => writeFile(join(homedir, '.angular.json'), '{"version":1}')) - .then(() => expectToFail(() => ng('new', 'proj-name', '--dry-run'))) - .then(() => deleteFile(join(homedir, '.angular.json'))); -} diff --git a/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts b/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts deleted file mode 100644 index 433511947929..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/ngcc-es2015-only.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { ng } from '../../utils/process'; - -export default async function() { - const { stderr, stdout } = await ng('build'); - - if (stdout.includes('as esm5') || stderr.includes('as esm5')) { - throw new Error('ngcc should not process ES5 during differential loading builds.'); - } -} diff --git a/tests/legacy-cli/e2e/tests/basic/rebuild.ts b/tests/legacy-cli/e2e/tests/basic/rebuild.ts deleted file mode 100644 index 261835bcc09f..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/rebuild.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { - killAllProcesses, - waitForAnyProcessOutputToMatch, - execAndWaitForOutputToMatch, - ng, -} from '../../utils/process'; -import { writeFile, writeMultipleFiles } from '../../utils/fs'; -import { wait } from '../../utils/utils'; -import { request } from '../../utils/http'; - -const validBundleRegEx = / Compiled successfully./; - -export default function () { - return ( - execAndWaitForOutputToMatch('ng', ['serve'], validBundleRegEx) - // Add a lazy module. - .then(() => ng('generate', 'module', 'lazy', '--routing')) - // Should trigger a rebuild with a new bundle. - // We need to use Promise.all to ensure we are waiting for the rebuild just before we write - // the file, otherwise rebuilds can be too fast and fail CI. - .then(() => - Promise.all([ - waitForAnyProcessOutputToMatch(validBundleRegEx, 20000), - writeFile( - 'src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { FormsModule } from '@angular/forms'; - import { HttpClientModule } from '@angular/common/http'; - - import { AppComponent } from './app.component'; - import { RouterModule } from '@angular/router'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpClientModule, - RouterModule.forRoot([ - { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) } - ]) - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, - ), - ]), - ) - // Count the bundles. - .then((results) => { - const stdout = results[0].stdout; - if (!/lazy_module_ts\.js/g.test(stdout)) { - throw new Error('Expected webpack to create a new chunk, but did not.'); - } - }) - // Change multiple files and check that all of them are invalidated and recompiled. - .then(() => - Promise.all([ - waitForAnyProcessOutputToMatch(validBundleRegEx, 20000), - writeMultipleFiles({ - 'src/app/app.module.ts': ` - 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 { } - - console.log('$$_E2E_GOLDEN_VALUE_1'); - export let X = '$$_E2E_GOLDEN_VALUE_2'; - `, - 'src/main.ts': ` - import { enableProdMode } from '@angular/core'; - import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - platformBrowserDynamic().bootstrapModule(AppModule); - - import * as m from './app/app.module'; - console.log(m.X); - console.log('$$_E2E_GOLDEN_VALUE_3'); - `, - }), - ]), - ) - .then(() => - Promise.all([ - waitForAnyProcessOutputToMatch(validBundleRegEx, 20000), - writeMultipleFiles({ - 'src/app/app.module.ts': ` - - 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 { } - - console.log('$$_E2E_GOLDEN_VALUE_1'); - export let X = '$$_E2E_GOLDEN_VALUE_2'; - console.log('File changed with no import/export changes'); - `, - }), - ]), - ) - .then(() => wait(2000)) - .then(() => request('http://localhost:4200/main.js')) - .then((body) => { - if (!body.match(/\$\$_E2E_GOLDEN_VALUE_1/)) { - throw new Error('Expected golden value 1.'); - } - if (!body.match(/\$\$_E2E_GOLDEN_VALUE_2/)) { - throw new Error('Expected golden value 2.'); - } - if (!body.match(/\$\$_E2E_GOLDEN_VALUE_3/)) { - throw new Error('Expected golden value 3.'); - } - }) - .then(() => - Promise.all([ - waitForAnyProcessOutputToMatch(validBundleRegEx, 20000), - writeMultipleFiles({ - 'src/app/app.component.html': '

testingTESTING123

', - }), - ]), - ) - .then(() => wait(2000)) - .then(() => request('http://localhost:4200/main.js')) - .then((body) => { - if (!body.match(/testingTESTING123/)) { - throw new Error('Expected component HTML to update.'); - } - }) - .then(() => - Promise.all([ - waitForAnyProcessOutputToMatch(validBundleRegEx, 20000), - writeMultipleFiles({ - 'src/app/app.component.css': ':host { color: blue; }', - }), - ]), - ) - .then(() => wait(2000)) - .then(() => request('http://localhost:4200/main.js')) - .then((body) => { - if (!body.match(/color:\s?blue/)) { - throw new Error('Expected component CSS to update.'); - } - }) - .then( - () => killAllProcesses(), - (err: unknown) => { - killAllProcesses(); - throw err; - }, - ) - ); -} diff --git a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts b/tests/legacy-cli/e2e/tests/basic/scripts-array.ts deleted file mode 100644 index 614ae94de09d..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/scripts-array.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - await writeMultipleFiles({ - 'src/string-script.js': "console.log('string-script'); var number = 1+1;", - 'src/zstring-script.js': "console.log('zstring-script');", - 'src/fstring-script.js': "console.log('fstring-script');", - 'src/ustring-script.js': "console.log('ustring-script');", - 'src/bstring-script.js': "console.log('bstring-script');", - 'src/astring-script.js': "console.log('astring-script');", - 'src/cstring-script.js': "console.log('cstring-script');", - 'src/input-script.js': "console.log('input-script');", - 'src/lazy-script.js': "console.log('lazy-script');", - 'src/pre-rename-script.js': "console.log('pre-rename-script');", - 'src/pre-rename-lazy-script.js': "console.log('pre-rename-lazy-script');", - }); - - await appendToFile('src/main.ts', "import './string-script.js';"); - - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.build.options.scripts = [ - { input: 'src/string-script.js' }, - { input: 'src/zstring-script.js' }, - { input: 'src/fstring-script.js' }, - { input: 'src/ustring-script.js' }, - { input: 'src/bstring-script.js' }, - { input: 'src/astring-script.js' }, - { input: 'src/cstring-script.js' }, - { input: 'src/input-script.js' }, - { input: 'src/lazy-script.js', inject: false }, - { input: 'src/pre-rename-script.js', bundleName: 'renamed-script' }, - { - input: 'src/pre-rename-lazy-script.js', - bundleName: 'renamed-lazy-script', - inject: false, - }, - ]; - }); - - await ng('build', '--extract-css', '--configuration=development'); - - // files were created successfully - await expectFileToMatch('dist/test-project/scripts.js', 'string-script'); - await expectFileToMatch('dist/test-project/scripts.js', 'input-script'); - await expectFileToMatch('dist/test-project/lazy-script.js', 'lazy-script'); - await expectFileToMatch('dist/test-project/renamed-script.js', 'pre-rename-script'); - await expectFileToMatch('dist/test-project/renamed-lazy-script.js', 'pre-rename-lazy-script'); - - // index.html lists the right bundles - await expectFileToMatch( - 'dist/test-project/index.html', - oneLineTrim` - - - - - - - `, - ); -} diff --git a/tests/legacy-cli/e2e/tests/basic/serve.ts b/tests/legacy-cli/e2e/tests/basic/serve.ts deleted file mode 100644 index 03457450cae8..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/serve.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { request } from '../../utils/http'; -import { killAllProcesses } from '../../utils/process'; -import { ngServe } from '../../utils/project'; - -export default async function () { - try { - // Serve works without HMR - await ngServe('--no-hmr'); - await verifyResponse(); - killAllProcesses(); - - // Serve works with HMR - await ngServe('--hmr'); - await verifyResponse(); - } finally { - killAllProcesses(); - } -} - -async function verifyResponse(): Promise { - const response = await request('http://localhost:4200/'); - - if (!/<\/app-root>/.test(response)) { - throw new Error('Response does not match expected value.'); - } -} diff --git a/tests/legacy-cli/e2e/tests/basic/size-tracking.ts b/tests/legacy-cli/e2e/tests/basic/size-tracking.ts deleted file mode 100644 index d648b50f6639..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/size-tracking.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { appendToFile, moveDirectory, prependToFile, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default async function () { - // Store the production build for artifact storage on CircleCI - if (process.env['CIRCLECI']) { - - // Add initial app routing. - // This is done automatically on a new app with --routing but must be done manually on - // existing apps. - const appRoutingModulePath = 'src/app/app-routing.module.ts'; - await writeFile(appRoutingModulePath, ` - import { NgModule } from '@angular/core'; - import { Routes, RouterModule } from '@angular/router'; - - const routes: Routes = []; - - @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] - }) - export class AppRoutingModule { } - `); - await prependToFile('src/app/app.module.ts', - `import { AppRoutingModule } from './app-routing.module';`); - await replaceInFile('src/app/app.module.ts', `imports: [`, `imports: [ AppRoutingModule,`); - await appendToFile('src/app/app.component.html', ''); - - // Add a lazy module. - await ng('generate', 'module', 'lazy', '--route=lazy', '--module=app.module'); - - // Build without hashing and with named chunks to keep have consistent file names. - await ng('build', '--output-hashing=none', '--named-chunks=true'); - - // Upload to the store_artifacts dir listed in .circleci/config.yml - await moveDirectory('dist', '/tmp/dist'); - } -} diff --git a/tests/legacy-cli/e2e/tests/basic/styles-array.ts b/tests/legacy-cli/e2e/tests/basic/styles-array.ts deleted file mode 100644 index 92ce540c2fb6..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/styles-array.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - await writeMultipleFiles({ - 'src/string-style.css': '.string-style { color: red }', - 'src/input-style.css': '.input-style { color: red }', - 'src/lazy-style.css': '.lazy-style { color: red }', - 'src/pre-rename-style.css': '.pre-rename-style { color: red }', - 'src/pre-rename-lazy-style.css': '.pre-rename-lazy-style { color: red }', - }); - - await updateJsonFile('angular.json', (workspaceJson) => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/string-style.css' }, - { input: 'src/input-style.css' }, - { input: 'src/lazy-style.css', inject: false }, - { input: 'src/pre-rename-style.css', bundleName: 'renamed-style' }, - { - input: 'src/pre-rename-lazy-style.css', - bundleName: 'renamed-lazy-style', - inject: false, - }, - ]; - }); - - const { stdout } = await ng('build', '--extract-css', '--configuration=development'); - - await expectFileToMatch('dist/test-project/styles.css', '.string-style'); - await expectFileToMatch('dist/test-project/styles.css', '.input-style'); - await expectFileToMatch('dist/test-project/lazy-style.css', '.lazy-style'); - await expectFileToMatch('dist/test-project/renamed-style.css', '.pre-rename-style'); - await expectFileToMatch('dist/test-project/renamed-lazy-style.css', '.pre-rename-lazy-style'); - await expectFileToMatch( - 'dist/test-project/index.html', - oneLineTrim` - - - `, - ); - - // Non injected styles should be listed under lazy chunk files - if (!/Lazy Chunk Files.*\srenamed-lazy-style\.css/m.test(stdout)) { - throw new Error(`Expected "renamed-lazy-style.css" to be listed under "Lazy Chunk Files".`); - } -} diff --git a/tests/legacy-cli/e2e/tests/basic/test.ts b/tests/legacy-cli/e2e/tests/basic/test.ts deleted file mode 100644 index 9ae72b9026d1..000000000000 --- a/tests/legacy-cli/e2e/tests/basic/test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ng } from '../../utils/process'; -import { moveFile } from '../../utils/fs'; - -export default function () { - // make sure both --watch=false work - return ng('test', '--watch=false') - .then(() => moveFile('./karma.conf.js', './karma.conf.bis.js')) - .then(() => ng('test', '--watch=false', '--karmaConfig=karma.conf.bis.js')); -} diff --git a/tests/legacy-cli/e2e/tests/build/allow-js.ts b/tests/legacy-cli/e2e/tests/build/allow-js.ts deleted file mode 100644 index 4025a00f6ce0..000000000000 --- a/tests/legacy-cli/e2e/tests/build/allow-js.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ng } from '../../utils/process'; -import { updateTsConfig } from '../../utils/project'; -import { appendToFile, writeFile } from '../../utils/fs'; - -export default async function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - await writeFile('src/my-js-file.js', 'console.log(1); export const a = 2;'); - await appendToFile('src/main.ts', ` - import { a } from './my-js-file'; - console.log(a); - `); - - await updateTsConfig(json => { - json['compilerOptions'].allowJs = true; - }); - - await ng('build', '--configuration=development'); - await ng('build', '--aot', '--configuration=development'); -} diff --git a/tests/legacy-cli/e2e/tests/build/assets.ts b/tests/legacy-cli/e2e/tests/build/assets.ts deleted file mode 100644 index 5961a5ac587b..000000000000 --- a/tests/legacy-cli/e2e/tests/build/assets.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as fs from 'fs'; -import { expectFileToExist, expectFileToMatch, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - await writeFile('src/assets/.file', ''); - await writeFile('src/assets/test.abc', 'hello world'); - - await ng('build', '--configuration=development'); - - await expectFileToExist('dist/test-project/favicon.ico'); - await expectFileToExist('dist/test-project/assets/.file'); - await expectFileToMatch('dist/test-project/assets/test.abc', 'hello world'); - await expectToFail(() => expectFileToExist('dist/test-project/assets/.gitkeep')); - - // Ensure `followSymlinks` option follows symlinks - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect['build'].options.assets = [{ glob: '**/*', input: 'src/assets', output: 'assets', followSymlinks: true }]; - }); - fs.mkdirSync('dirToSymlink/subdir1', { recursive: true }); - fs.mkdirSync('dirToSymlink/subdir2/subsubdir1', { recursive: true }); - fs.writeFileSync('dirToSymlink/a.txt', ''); - fs.writeFileSync('dirToSymlink/subdir1/b.txt', ''); - fs.writeFileSync('dirToSymlink/subdir2/c.txt', ''); - fs.writeFileSync('dirToSymlink/subdir2/subsubdir1/d.txt', ''); - fs.symlinkSync(process.cwd() + '/dirToSymlink', 'src/assets/symlinkDir'); - - await ng('build', '--configuration=development'); - - await expectFileToExist('dist/test-project/assets/symlinkDir/a.txt'); - await expectFileToExist('dist/test-project/assets/symlinkDir/subdir1/b.txt'); - await expectFileToExist('dist/test-project/assets/symlinkDir/subdir2/c.txt'); - await expectFileToExist('dist/test-project/assets/symlinkDir/subdir2/subsubdir1/d.txt'); -} diff --git a/tests/legacy-cli/e2e/tests/build/barrel-file.ts b/tests/legacy-cli/e2e/tests/build/barrel-file.ts deleted file mode 100644 index 048ad4599a26..000000000000 --- a/tests/legacy-cli/e2e/tests/build/barrel-file.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function() { - await writeFile('src/app/index.ts', `export { AppModule } from './app.module';`); - await replaceInFile('src/main.ts', './app/app.module', './app'); - await ng('build', '--configuration=development'); -} diff --git a/tests/legacy-cli/e2e/tests/build/base-href.ts b/tests/legacy-cli/e2e/tests/build/base-href.ts deleted file mode 100644 index 610cb282a6d5..000000000000 --- a/tests/legacy-cli/e2e/tests/build/base-href.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ng } from '../../utils/process'; -import { expectFileToMatch } from '../../utils/fs'; - - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return ng('build', '--base-href', '/myUrl', '--configuration=development') - .then(() => expectFileToMatch('dist/test-project/index.html', //)); -} diff --git a/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts b/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts deleted file mode 100644 index 95ece32ea6f5..000000000000 --- a/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { appendToFile, expectFileToMatch } from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -const snapshots = require('../../ng-snapshot/package.json'); - -export default async function () { - await appendToFile('src/app/app.component.html', ''); - await ng('generate', 'appShell', '--project', 'test-project'); - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - if (isSnapshotBuild) { - const packagesToInstall = []; - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - // Iterate over all of the packages to update them to the snapshot version. - for (const [name, version] of Object.entries(snapshots.dependencies)) { - if (name in dependencies && dependencies[name] !== version) { - packagesToInstall.push(version); - } - } - }); - - for (const pkg of packagesToInstall) { - await installPackage(pkg); - } - } - - await ng('run', 'test-project:app-shell:development'); - await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); - - await ng('run', 'test-project:app-shell'); - await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); -} diff --git a/tests/legacy-cli/e2e/tests/build/build-app-shell.ts b/tests/legacy-cli/e2e/tests/build/build-app-shell.ts deleted file mode 100644 index 42d1a41bbd94..000000000000 --- a/tests/legacy-cli/e2e/tests/build/build-app-shell.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { stripIndent } from 'common-tags'; -import { getGlobalVariable } from '../../utils/env'; -import { expectFileToMatch, writeFile } from '../../utils/fs'; -import { installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { readNgVersion } from '../../utils/version'; - -export default function() { - let platformServerVersion = readNgVersion(); - - if (getGlobalVariable('argv')['ng-snapshots']) { - platformServerVersion = require('../../ng-snapshot/package.json') - .dependencies['@angular/platform-server']; - } - - return Promise.resolve() - .then(() => - updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect['server'] = { - builder: '@angular-devkit/build-angular:server', - options: { - outputPath: 'dist/test-project-server', - main: 'src/main.server.ts', - tsConfig: 'tsconfig.server.json', - }, - }; - appArchitect['app-shell'] = { - builder: '@angular-devkit/build-angular:app-shell', - options: { - browserTarget: 'test-project:build', - serverTarget: 'test-project:server', - route: '/shell', - }, - }; - }), - ) - .then(() => - writeFile( - './tsconfig.server.json', - ` - { - "extends": "./tsconfig.app.json", - "compilerOptions": { - "outDir": "../dist-server", - "baseUrl": "./", - "module": "commonjs", - "types": [] - }, - "files": [ - "src/main.server.ts" - ], - "include": [ - "src/**/*.d.ts" - ], - "angularCompilerOptions": { - "entryModule": "src/app/app.server.module#AppServerModule" - } - } - `, - ), - ) - .then(() => - writeFile( - './src/main.server.ts', - ` - import { enableProdMode } from '@angular/core'; - - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - - export { AppServerModule } from './app/app.server.module'; - export { renderModule, renderModuleFactory } from '@angular/platform-server'; - `, - ), - ) - .then(() => - writeFile( - './src/app/app.component.html', - stripIndent` - Hello World - - `, - ), - ) - .then(() => - writeFile( - './src/app/app.module.ts', - stripIndent` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { RouterModule } from '@angular/router'; - - import { AppComponent } from './app.component'; - - @NgModule({ - imports: [ - BrowserModule.withServerTransition({ appId: 'appshell-play' }), - RouterModule - ], - declarations: [AppComponent], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, - ), - ) - .then(() => - writeFile( - './src/app/app.server.module.ts', - stripIndent` - import {NgModule} from '@angular/core'; - import {ServerModule} from '@angular/platform-server'; - import { Routes, RouterModule } from '@angular/router'; - - import { AppModule } from './app.module'; - import { AppComponent } from './app.component'; - import { ShellComponent } from './shell.component'; - - const routes: Routes = [ - { path: 'shell', component: ShellComponent } - ]; - - @NgModule({ - imports: [ - // The AppServerModule should import your AppModule followed - // by the ServerModule from @angular/platform-server. - AppModule, - ServerModule, - RouterModule.forRoot(routes), - ], - // Since the bootstrapped component is not inherited from your - // imported AppModule, it needs to be repeated here. - bootstrap: [AppComponent], - declarations: [ShellComponent], - }) - export class AppServerModule {} - `, - ), - ) - .then(() => - writeFile( - './src/app/shell.component.ts', - stripIndent` - import { Component } from '@angular/core'; - @Component({ - selector: 'app-shell', - template: '

shell Works!

', - styles: [] - }) - export class ShellComponent {} - `, - ), - ) - .then(() => - updateJsonFile('package.json', packageJson => { - const dependencies = packageJson['dependencies']; - dependencies['@angular/platform-server'] = platformServerVersion; - }).then(() => installWorkspacePackages()), - ) - .then(() => ng('run', 'test-project:app-shell')) - .then(() => expectFileToMatch('dist/test-project/index.html', /shell Works!/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/build-errors.ts b/tests/legacy-cli/e2e/tests/build/build-errors.ts deleted file mode 100644 index b8abc5b4dad3..000000000000 --- a/tests/legacy-cli/e2e/tests/build/build-errors.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { writeFile, appendToFile, readFile, replaceInFile } from '../../utils/fs'; -import { getGlobalVariable } from '../../utils/env'; -import { expectToFail } from '../../utils/utils'; - -const extraErrors = [ - `Final loader didn't return a Buffer or String`, - `doesn't contain a valid alias configuration`, - `main.ts is not part of the TypeScript compilation.`, -]; - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - if (process.platform.startsWith('win')) { - return Promise.resolve(); - } - - // Skip this test in Angular 2/4. - if (getGlobalVariable('argv').ng2 || getGlobalVariable('argv').ng4) { - return Promise.resolve(); - } - - let origContent: string; - - return ( - Promise.resolve() - // Save the original contents of `./src/app/app.component.ts`. - .then(() => readFile('./src/app/app.component.ts')) - .then(contents => (origContent = contents)) - // Check `part of the TypeScript compilation` errors. - // These should show an error only for the missing file. - .then(() => - updateJsonFile('./tsconfig.app.json', configJson => { - (configJson.include = undefined), (configJson.files = ['src/main.ts']); - }), - ) - .then(() => expectToFail(() => ng('build'))) - .then(({ message }) => { - if (!message.includes('polyfills.ts is missing from the TypeScript compilation')) { - throw new Error(`Expected missing TS file error, got this instead:\n${message}`); - } - if (extraErrors.some(e => message.includes(e))) { - throw new Error(`Did not expect extra errors but got:\n${message}`); - } - }) - .then(() => - updateJsonFile('./tsconfig.app.json', configJson => { - configJson.include = ['src/**/*.ts']; - configJson.exclude = ['**/**.spec.ts']; - configJson.files = undefined; - }), - ) - // Check simple single syntax errors. - // These shouldn't skip emit and just show a TS error. - .then(() => appendToFile('./src/app/app.component.ts', ']]]')) - .then(() => expectToFail(() => ng('build'))) - .then(({ message }) => { - if (!message.includes('Declaration or statement expected.')) { - throw new Error(`Expected syntax error, got this instead:\n${message}`); - } - if (extraErrors.some(e => message.includes(e))) { - throw new Error(`Did not expect extra errors but got:\n${message}`); - } - }) - .then(() => writeFile('./src/app/app.component.ts', origContent)) - // Check errors when files were not emitted due to static analysis errors. - .then(() => replaceInFile('./src/app/app.component.ts', `'app-root'`, `(() => 'app-root')()`)) - .then(() => expectToFail(() => ng('build', '--aot'))) - .then(({ message }) => { - if ( - !message.includes('Function calls are not supported') && - !message.includes('Function expressions are not supported in decorators') && - !message.includes('selector must be a string') - ) { - throw new Error(`Expected static analysis error, got this instead:\n${message}`); - } - if (extraErrors.some(e => message.includes(e))) { - throw new Error(`Did not expect extra errors but got:\n${message}`); - } - }) - .then(() => writeFile('./src/app/app.component.ts', origContent)) - ); -} \ No newline at end of file diff --git a/tests/legacy-cli/e2e/tests/build/build-optimizer.ts b/tests/legacy-cli/e2e/tests/build/build-optimizer.ts deleted file mode 100644 index 2cdbeceb868a..000000000000 --- a/tests/legacy-cli/e2e/tests/build/build-optimizer.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ng } from '../../utils/process'; -import { expectFileToMatch, expectFileToExist } from '../../utils/fs'; -import { expectToFail } from '../../utils/utils'; - - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return ng('build', '--aot', '--build-optimizer') - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/main.js', /\.decorators =/))) - .then(() => ng('build')) - .then(() => expectToFail(() => expectFileToExist('dist/vendor.js'))) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/main.js', /\.decorators =/))) - .then(() => expectToFail(() => ng('build', '--aot=false', '--build-optimizer'))); -} diff --git a/tests/legacy-cli/e2e/tests/build/bundle-budgets.ts b/tests/legacy-cli/e2e/tests/build/bundle-budgets.ts deleted file mode 100644 index 8b41f0596dcf..000000000000 --- a/tests/legacy-cli/e2e/tests/build/bundle-budgets.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - // Error - await updateJsonFile('angular.json', json => { - json.projects['test-project'].architect.build.configurations.production.budgets = [ - { type: 'all', maximumError: '100b' }, - ]; - }); - - const errorMessage = await expectToFail(() => ng('build')); - if (!/Error.+budget/.test(errorMessage)) { - throw new Error('Budget error: all, max error.'); - } - - // Warning - await updateJsonFile('angular.json', json => { - json.projects['test-project'].architect.build.configurations.production.budgets = [ - { type: 'all', minimumWarning: '100mb' }, - ]; - }); - - const { stderr } = await ng('build'); - if (!/Warning.+budget/.test(stderr)) { - throw new Error('Budget warning: all, min warning'); - } - - // Pass - await updateJsonFile('angular.json', json => { - json.projects['test-project'].architect.build.configurations.production.budgets = [ - { type: 'allScript', maximumError: '100mb' }, - ]; - }); - - const { stderr: stderr2 } = await ng('build'); - if (/(Warning|Error)/.test(stderr2)) { - throw new Error('BIG max for all, should not error'); - } -} diff --git a/tests/legacy-cli/e2e/tests/build/chunk-hash.ts b/tests/legacy-cli/e2e/tests/build/chunk-hash.ts deleted file mode 100644 index 0c119a65ee78..000000000000 --- a/tests/legacy-cli/e2e/tests/build/chunk-hash.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as fs from 'fs'; - -import {ng} from '../../utils/process'; -import {writeFile, prependToFile, replaceInFile} from '../../utils/fs'; - -const OUTPUT_RE = /(main|polyfills|vendor|inline|styles|\d+)\.[a-z0-9]+\.(chunk|bundle)\.(js|css)$/; - -function generateFileHashMap(): Map { - const hashes = new Map(); - - fs.readdirSync('./dist') - .forEach(name => { - if (!name.match(OUTPUT_RE)) { - return; - } - - const [module, hash] = name.split('.'); - hashes.set(module, hash); - }); - - return hashes; -} - -function validateHashes( - oldHashes: Map, - newHashes: Map, - shouldChange: Array): void { - - console.log(' Validating hashes...'); - console.log(` Old hashes: ${JSON.stringify([...oldHashes])}`); - console.log(` New hashes: ${JSON.stringify([...newHashes])}`); - - oldHashes.forEach((hash, module) => { - if (hash == newHashes.get(module)) { - if (shouldChange.includes(module)) { - throw new Error(`Module "${module}" did not change hash (${hash})...`); - } - } else if (!shouldChange.includes(module)) { - throw new Error(`Module "${module}" changed hash (${hash})...`); - } - }); -} - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - return; - - let oldHashes: Map; - let newHashes: Map; - // First, collect the hashes. - return Promise.resolve() - .then(() => ng('generate', 'module', 'lazy', '--routing')) - .then(() => prependToFile('src/app/app.module.ts', ` - import { RouterModule } from '@angular/router'; - import { ReactiveFormsModule } from '@angular/forms'; - `)) - .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ - RouterModule.forRoot([{ path: "lazy", loadChildren: "./lazy/lazy.module#LazyModule" }]), - ReactiveFormsModule, - `)) - .then(() => ng('build', '--output-hashing=all', '--configuration=development')) - .then(() => { - oldHashes = generateFileHashMap(); - }) - .then(() => ng('build', '--output-hashing=all', '--configuration=development')) - .then(() => { - newHashes = generateFileHashMap(); - }) - .then(() => { - validateHashes(oldHashes, newHashes, []); - oldHashes = newHashes; - }) - .then(() => writeFile('src/styles.css', 'body { background: blue; }')) - .then(() => ng('build', '--output-hashing=all', '--configuration=development')) - .then(() => { - newHashes = generateFileHashMap(); - }) - .then(() => { - validateHashes(oldHashes, newHashes, ['styles']); - oldHashes = newHashes; - }) - .then(() => writeFile('src/app/app.component.css', 'h1 { margin: 10px; }')) - .then(() => ng('build', '--output-hashing=all', '--configuration=development')) - .then(() => { - newHashes = generateFileHashMap(); - }) - .then(() => { - validateHashes(oldHashes, newHashes, ['main']); - oldHashes = newHashes; - }) - .then(() => prependToFile('src/app/lazy/lazy.module.ts', ` - import { ReactiveFormsModule } from '@angular/forms'; - `)) - .then(() => replaceInFile('src/app/lazy/lazy.module.ts', 'imports: [', ` - imports: [ - ReactiveFormsModule, - `)) - .then(() => ng('build', '--output-hashing=all', '--configuration=development')) - .then(() => { - newHashes = generateFileHashMap(); - }) - .then(() => { - validateHashes(oldHashes, newHashes, ['inline', '0']); - }); -} diff --git a/tests/legacy-cli/e2e/tests/build/config-file-fallback.ts b/tests/legacy-cli/e2e/tests/build/config-file-fallback.ts deleted file mode 100644 index 3dd987431b8a..000000000000 --- a/tests/legacy-cli/e2e/tests/build/config-file-fallback.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {ng} from '../../utils/process'; -import {moveFile} from '../../utils/fs'; - - -export default function() { - return Promise.resolve() - .then(() => ng('build')) - .then(() => moveFile('angular.json', '.angular.json')) - .then(() => ng('build')); -} diff --git a/tests/legacy-cli/e2e/tests/build/css-urls.ts b/tests/legacy-cli/e2e/tests/build/css-urls.ts deleted file mode 100644 index f2dc6ee838ad..000000000000 --- a/tests/legacy-cli/e2e/tests/build/css-urls.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ng } from '../../utils/process'; -import { - expectFileToMatch, - expectFileToExist, - expectFileMatchToExist, - writeMultipleFiles -} from '../../utils/fs'; -import { copyProjectAsset } from '../../utils/assets'; -import { expectToFail } from '../../utils/utils'; - -const imgSvg = ` - - - -`; - -export default function () { - return Promise.resolve() - // Verify absolute/relative paths in global/component css. - .then(() => writeMultipleFiles({ - 'src/styles.css': ` - h1 { background: url('/assets/global-img-absolute.svg'); } - h2 { background: url('./assets/global-img-relative.png'); } - `, - 'src/app/app.component.css': ` - h3 { background: url('/assets/component-img-absolute.svg'); } - h4 { background: url('../assets/component-img-relative.png'); } - `, - 'src/assets/global-img-absolute.svg': imgSvg, - 'src/assets/component-img-absolute.svg': imgSvg - })) - .then(() => copyProjectAsset('images/spectrum.png', './src/assets/global-img-relative.png')) - .then(() => copyProjectAsset('images/spectrum.png', './src/assets/component-img-relative.png')) - .then(() => ng('build', '--extract-css', '--aot', '--configuration=development')) - // Check paths are correctly generated. - .then(() => expectFileToMatch('dist/test-project/styles.css', 'assets/global-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\('\/assets\/global-img-absolute\.svg'\)/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /global-img-relative\.png/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - '/assets/component-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/main.js', - /component-img-relative\.png/)) - // Check files are correctly created. - .then(() => expectToFail(() => expectFileToExist('dist/test-project/global-img-absolute.svg'))) - .then(() => expectToFail(() => expectFileToExist('dist/test-project/component-img-absolute.svg'))) - .then(() => expectFileMatchToExist('./dist/test-project', /global-img-relative\.png/)) - .then(() => expectFileMatchToExist('./dist/test-project', /component-img-relative\.png/)) - // Check urls with deploy-url scheme are used as is. - .then(() => ng('build', '--base-href=/base/', '--deploy-url=http://deploy.url/', - '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(\'\/assets\/global-img-absolute\.svg\'\)/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /url\(\'\/assets\/component-img-absolute\.svg\'\)/)) - // Check urls with base-href scheme are used as is (with deploy-url). - .then(() => ng('build', '--base-href=http://base.url/', '--deploy-url=deploy/', - '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(\'\/assets\/global-img-absolute\.svg\'\)/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /url\(\'\/assets\/component-img-absolute\.svg\'\)/)) - // Check urls with deploy-url and base-href scheme only use deploy-url. - .then(() => ng('build', '--base-href=http://base.url/', '--deploy-url=http://deploy.url/', - '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(\'\/assets\/global-img-absolute\.svg\'\)/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /url\(\'\/assets\/component-img-absolute\.svg\'\)/)) - // Check with base-href and deploy-url flags. - .then(() => ng('build', '--base-href=/base/', '--deploy-url=deploy/', - '--extract-css', '--aot', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - '/assets/global-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /global-img-relative\.png/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - '/assets/component-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/main.js', - /deploy\/component-img-relative\.png/)) - // Check with identical base-href and deploy-url flags. - .then(() => ng('build', '--base-href=/base/', '--deploy-url=/base/', - '--extract-css', '--aot', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - '/assets/global-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /global-img-relative\.png/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - '/assets/component-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/main.js', - /\/base\/component-img-relative\.png/)) - // Check with only base-href flag. - .then(() => ng('build', '--base-href=/base/', - '--extract-css', '--aot', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - '/assets/global-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /global-img-relative\.png/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - '/assets/component-img-absolute.svg')) - .then(() => expectFileToMatch('dist/test-project/main.js', - /component-img-relative\.png/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/delete-output-path.ts b/tests/legacy-cli/e2e/tests/build/delete-output-path.ts deleted file mode 100644 index 62f06fef8700..000000000000 --- a/tests/legacy-cli/e2e/tests/build/delete-output-path.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {ng} from '../../utils/process'; -import {expectToFail} from '../../utils/utils'; -import {deleteFile, expectFileToExist} from '../../utils/fs'; -import {getGlobalVariable} from '../../utils/env'; - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return ng('build') - // This is supposed to fail since there's a missing file - .then(() => deleteFile('src/app/app.component.ts')) - // The build fails but we don't delete the output of the previous build. - .then(() => expectToFail(() => ng('build', '--delete-output-path=false'))) - .then(() => expectFileToExist('dist')) - // By default, output path is always cleared. - .then(() => expectToFail(() => ng('build', '--configuration=development'))) - .then(() => expectToFail(() => expectFileToExist('dist/test-project'))); -} diff --git a/tests/legacy-cli/e2e/tests/build/deploy-url.ts b/tests/legacy-cli/e2e/tests/build/deploy-url.ts deleted file mode 100644 index ca9a82ae3f03..000000000000 --- a/tests/legacy-cli/e2e/tests/build/deploy-url.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ng } from '../../utils/process'; -import { copyProjectAsset } from '../../utils/assets'; -import { appendToFile, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; - -export default function () { - return Promise.resolve() - .then(() => writeMultipleFiles({ - 'src/styles.css': 'div { background: url("./assets/more.png"); }', - 'src/lazy.ts': 'export const lazy = "lazy";', - })) - .then(() => appendToFile('src/main.ts', 'import("./lazy");')) - // use image with file size >10KB to prevent inlining - .then(() => copyProjectAsset('images/spectrum.png', './src/assets/more.png')) - .then(() => ng('build', '--deploy-url=deployUrl/', '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/index.html', 'deployUrl/main.js')) - // verify --deploy-url isn't applied to extracted css urls - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(['"]?more\.png['"]?\)/)) - .then(() => ng('build', '--deploy-url=http://example.com/some/path/', '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/index.html', 'http://example.com/some/path/main.js')) - // verify --deploy-url is applied to non-extracted css urls - .then(() => ng('build', '--deploy-url=deployUrl/', '--extract-css=false', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.js', - /\(['"]?deployUrl\/more\.png['"]?\)/)) - .then(() => expectFileToMatch('dist/test-project/runtime.js', - /__webpack_require__\.p\s*=\s*"deployUrl\/";/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-cache.ts b/tests/legacy-cli/e2e/tests/build/differential-cache.ts deleted file mode 100644 index 5ac16a5fcfac..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-cache.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import { rimraf, replaceInFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -function generateFileHashMap(): Map { - const hashes = new Map(); - - fs.readdirSync('./dist/test-project').forEach(name => { - const data = fs.readFileSync('./dist/test-project/' + name); - const hash = crypto - .createHash('sha1') - .update(data) - .digest('hex'); - - hashes.set(name, hash); - }); - - return hashes; -} - -function validateHashes( - oldHashes: Map, - newHashes: Map, - shouldChange: Array, -): void { - oldHashes.forEach((hash, name) => { - if (hash === newHashes.get(name)) { - if (shouldChange.includes(name)) { - throw new Error(`"${name}" did not change hash (${hash})...`); - } - } else if (!shouldChange.includes(name)) { - throw new Error(`"${name}" changed hash (${hash})...`); - } - }); -} - -export default async function() { - // Skip on CI due to large variability of performance - if (process.env['CI']) { - return; - } - - let oldHashes: Map; - let newHashes: Map; - - // Enable Differential loading to run both size checks - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - // Remove the cache so that an initial build and build with cache can be tested - await rimraf('./node_modules/.cache'); - - let start = Date.now(); - await ng('build', '--configuration=development'); - let initial = Date.now() - start; - oldHashes = generateFileHashMap(); - - start = Date.now(); - await ng('build', '--configuration=development'); - let cached = Date.now() - start; - newHashes = generateFileHashMap(); - - validateHashes(oldHashes, newHashes, []); - - if (cached > initial * 0.70) { - throw new Error( - `Cached build time [${cached}] should not be greater than 70% of initial build time [${initial}].`, - ); - } - - // Remove the cache so that an initial build and build with cache can be tested - await rimraf('./node_modules/.cache'); - - start = Date.now(); - await ng('build'); - initial = Date.now() - start; - oldHashes = generateFileHashMap(); - - start = Date.now(); - await ng('build'); - cached = Date.now() - start; - newHashes = generateFileHashMap(); - - if (cached > initial * 0.70) { - throw new Error( - `Cached build time [${cached}] should not be greater than 70% of initial build time [${initial}].`, - ); - } - - validateHashes(oldHashes, newHashes, []); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts b/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts deleted file mode 100644 index 42ddfe0368cf..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createHash } from 'crypto'; -import { - appendToFile, - expectFileToMatch, - prependToFile, - readFile, - replaceInFile, - writeFile, -} from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - // Enable Differential loading - await replaceInFile('.browserslistrc', 'not IE 11', 'IE 11'); - - const appRoutingModulePath = 'src/app/app-routing.module.ts'; - - // Add app routing. - // This is done automatically on a new app with --routing. - await writeFile( - appRoutingModulePath, - ` - import { NgModule } from '@angular/core'; - import { Routes, RouterModule } from '@angular/router'; - - const routes: Routes = []; - - @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] - }) - export class AppRoutingModule { } - `, - ); - await prependToFile( - 'src/app/app.module.ts', - `import { AppRoutingModule } from './app-routing.module';`, - ); - await replaceInFile('src/app/app.module.ts', `imports: [`, `imports: [ AppRoutingModule,`); - await appendToFile('src/app/app.component.html', ''); - - await ng('generate', 'module', 'lazy', '--module=app.module', '--route', 'lazy'); - - await ng('build', '--subresource-integrity', '--output-hashing=none', '--output-path=dist/first'); - - // Second build used to ensure cached files use correct integrity values - await ng( - 'build', - '--subresource-integrity', - '--output-hashing=none', - '--output-path=dist/second', - ); - - const chunkId = '86'; - const codeHashES5 = createHash('sha384') - .update(await readFile(`dist/first/${chunkId}-es5.js`)) - .digest('base64'); - const codeHashes2017 = createHash('sha384') - .update(await readFile(`dist/first/${chunkId}-es2017.js`)) - .digest('base64'); - - await expectFileToMatch('dist/first/runtime-es5.js', 'sha384-' + codeHashES5); - await expectFileToMatch('dist/first/runtime-es2017.js', 'sha384-' + codeHashes2017); - - await expectFileToMatch('dist/second/runtime-es5.js', 'sha384-' + codeHashES5); - await expectFileToMatch('dist/second/runtime-es2017.js', 'sha384-' + codeHashes2017); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts b/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts deleted file mode 100644 index ebf2b6ff2b35..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-watch.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expectFileToExist, replaceInFile } from '../../utils/fs'; -import { execAndWaitForOutputToMatch } from '../../utils/process'; - -export default async function () { - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - await execAndWaitForOutputToMatch('ng', ['build', '--watch', '--configuration=development'], /Initial Total/i); - await expectFileToExist('dist/test-project/runtime-es2017.js'); - await expectFileToExist('dist/test-project/main-es2017.js'); -} diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading.ts b/tests/legacy-cli/e2e/tests/build/differential-loading.ts deleted file mode 100644 index d3fc49dfe68f..000000000000 --- a/tests/legacy-cli/e2e/tests/build/differential-loading.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { appendToFile, expectFileToMatch, replaceInFile, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - // Enable Differential loading to run both size checks - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - await writeMultipleFiles({ - 'src/string-script.js': "console.log('string-script'); var number = 1+1;", - 'src/pre-rename-script.js': "console.log('pre-rename-script');", - }); - - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.build.options.scripts = [ - { input: 'src/string-script.js' }, - { input: 'src/pre-rename-script.js', bundleName: 'renamed-script' }, - ]; - }); - - await ng('build', '--extract-css', '--vendor-chunk', '--optimization', '--configuration=development'); - - // index.html lists the right bundles - await expectFileToMatch( - 'dist/test-project/index.html', - oneLineTrim` - - - - - - - - - - - `, - ); - - await expectFileToMatch('dist/test-project/vendor-es2017.js', /class \w{constructor\(/); - await expectToFail(() => expectFileToMatch('dist/test-project/vendor-es5.js', /class \w{constructor\(/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/extract-licenses.ts b/tests/legacy-cli/e2e/tests/build/extract-licenses.ts deleted file mode 100644 index 7d0b61d501f4..000000000000 --- a/tests/legacy-cli/e2e/tests/build/extract-licenses.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expectFileToExist, expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function() { - // Licenses should be left intact if extraction is disabled - await ng('build', '--extract-licenses=false', '--output-hashing=none'); - - await expectToFail(() => expectFileToExist('dist/test-project/3rdpartylicenses.txt')); - await expectFileToMatch('dist/test-project/main.js', '@license'); - - // Licenses should be removed if extraction is enabled - await ng('build', '--extract-licenses', '--output-hashing=none'); - - await expectFileToExist('dist/test-project/3rdpartylicenses.txt'); - await expectToFail(() => expectFileToMatch('dist/test-project/main.js', '@license')); -} diff --git a/tests/legacy-cli/e2e/tests/build/jit-prod.ts b/tests/legacy-cli/e2e/tests/build/jit-prod.ts deleted file mode 100644 index 66162c5a52c7..000000000000 --- a/tests/legacy-cli/e2e/tests/build/jit-prod.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - - -export default async function () { - // Make prod use JIT. - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.build.configurations['production'].aot = false; - appArchitect.build.configurations['production'].buildOptimizer = false; - }); - - // Test it works - await ng('e2e', '--configuration=production'); -} diff --git a/tests/legacy-cli/e2e/tests/build/json.ts b/tests/legacy-cli/e2e/tests/build/json.ts deleted file mode 100644 index dd8e78f8b5e7..000000000000 --- a/tests/legacy-cli/e2e/tests/build/json.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expectFileToExist } from '../../utils/fs'; -import { expectGitToBeClean } from '../../utils/git'; -import { ng } from '../../utils/process'; - -export default async function() { - await ng('build', '--stats-json', '--configuration=development'); - await expectFileToExist('./dist/test-project/stats.json'); - await expectGitToBeClean(); -} diff --git a/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts b/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts deleted file mode 100644 index ed39e98b4b6a..000000000000 --- a/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { appendToFile, prependToFile, readFile, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - const projectName = 'test-project'; - const appRoutingModulePath = 'src/app/app-routing.module.ts'; - - // Add app routing. - // This is done automatically on a new app with --routing. - await writeFile(appRoutingModulePath, ` - import { NgModule } from '@angular/core'; - import { Routes, RouterModule } from '@angular/router'; - - const routes: Routes = []; - - @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] - }) - export class AppRoutingModule { } - `); - await prependToFile('src/app/app.module.ts', - `import { AppRoutingModule } from './app-routing.module';`); - await replaceInFile('src/app/app.module.ts', `imports: [`, `imports: [ AppRoutingModule,`); - await appendToFile('src/app/app.component.html', ''); - - const originalAppRoutingModule = await readFile(appRoutingModulePath); - // helper to replace loadChildren - const replaceLoadChildren = async (route: string) => { - const content = originalAppRoutingModule.replace('const routes: Routes = [];', ` - const routes: Routes = [{ path: 'lazy', loadChildren: ${route} }]; - `); - - return writeFile(appRoutingModulePath, content); - }; - - // Add lazy route. - await ng('generate', 'module', 'lazy', '--routing'); - await ng('generate', 'component', 'lazy/lazy-comp'); - await replaceInFile('src/app/lazy/lazy-routing.module.ts', 'const routes: Routes = [];', ` - import { LazyCompComponent } from './lazy-comp/lazy-comp.component'; - const routes: Routes = [{ path: '', component: LazyCompComponent }]; - `); - - // Add lazy route e2e - await writeFile('e2e/src/app.e2e-spec.ts', ` - import { browser, logging, element, by } from 'protractor'; - - describe('workspace-project App', () => { - it('should display lazy route', async () => { - await browser.get(browser.baseUrl + '/lazy'); - expect(await element(by.css('app-lazy-comp p')).getText()).toEqual('lazy-comp works!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - })); - }); - }); - `); - - // Convert the default config to use JIT and prod to just do AOT. - // This way we can use `ng e2e` to test JIT and `ng e2e --prod` to test AOT. - await updateJsonFile('angular.json', json => { - const buildTarget = json['projects'][projectName]['architect']['build']; - buildTarget['options']['aot'] = true; - buildTarget['configurations']['development']['aot'] = false; - }); - - // Test `import()` style lazy load. - // Both Ivy and View Engine should support it. - await replaceLoadChildren(`() => import('./lazy/lazy.module').then(m => m.LazyModule)`); - - await ng('e2e'); - await ng('e2e', '--configuration=production'); -} diff --git a/tests/legacy-cli/e2e/tests/build/material.ts b/tests/legacy-cli/e2e/tests/build/material.ts deleted file mode 100644 index 5963d3411d89..000000000000 --- a/tests/legacy-cli/e2e/tests/build/material.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { replaceInFile } from '../../utils/fs'; -import { installPackage, installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { isPrereleaseCli, updateJsonFile } from '../../utils/project'; - -const snapshots = require('../../ng-snapshot/package.json'); - -export default async function () { - const tag = await isPrereleaseCli() ? '@next' : ''; - await ng('add', `@angular/material${tag}`, '--skip-confirmation'); - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - if (isSnapshotBuild) { - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - // Angular material adds dependencies on other Angular packages - // Iterate over all of the packages to update them to the snapshot version. - for (const [name, version] of Object.entries(snapshots.dependencies)) { - if (name in dependencies) { - dependencies[name] = version; - } - } - - dependencies['@angular/material-moment-adapter'] = - snapshots.dependencies['@angular/material-moment-adapter']; - }); - - await installWorkspacePackages(); - } else { - await installPackage('@angular/material-moment-adapter'); - } - - await installPackage('moment'); - - await ng('build'); - - // Ensure moment adapter works (uses unique importing mechanism for moment) - // Issue: https://github.com/angular/angular-cli/issues/17320 - await replaceInFile( - 'src/app/app.module.ts', - `import { AppComponent } from './app.component';`, - ` - import { AppComponent } from './app.component'; - import { - MomentDateAdapter, - MAT_MOMENT_DATE_FORMATS - } from '@angular/material-moment-adapter'; - import { - DateAdapter, - MAT_DATE_LOCALE, - MAT_DATE_FORMATS - } from '@angular/material/core'; - `, - ); - - await replaceInFile( - 'src/app/app.module.ts', - `providers: []`, - ` - providers: [ - { - provide: DateAdapter, - useClass: MomentDateAdapter, - deps: [MAT_DATE_LOCALE] - }, - { - provide: MAT_DATE_FORMATS, - useValue: MAT_MOMENT_DATE_FORMATS - } - ] - `, - ); - - await ng('e2e', '--prod'); -} diff --git a/tests/legacy-cli/e2e/tests/build/multiple-configs.ts b/tests/legacy-cli/e2e/tests/build/multiple-configs.ts deleted file mode 100644 index 6897189758de..000000000000 --- a/tests/legacy-cli/e2e/tests/build/multiple-configs.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { expectFileToExist } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - // These are the default options, that we'll overwrite in subsequent configs. - // extractCss defaults to false - // sourceMap defaults to true - appArchitect['build'] = { - ...appArchitect['build'], - defaultConfiguration: undefined, - options: { - ...appArchitect['build'].options, - buildOptimizer: false, - optimization: false, - sourceMap: true, - outputHashing: 'none', - vendorChunk: true, - assets: [ - 'src/favicon.ico', - 'src/assets', - ], - styles: [ - 'src/styles.css', - ], - scripts: [], - budgets: [], - }, - configurations: { - development: { - sourceMap: true, - extractCss: false, - }, - one: { - assets: [], - }, - two: { - sourceMap: false, - }, - three: { - extractCss: false, // Defaults to false when not set. - }, - }, - }; - - return workspaceJson; - }); - - // Test the base configuration. - await ng('build', '--configuration=development'); - await expectFileToExist('dist/test-project/favicon.ico'); - await expectFileToExist('dist/test-project/main.js.map'); - await expectFileToExist('dist/test-project/styles.js'); - await expectFileToExist('dist/test-project/vendor.js'); - await ng('build'); - await expectFileToExist('dist/test-project/styles.css'); - // But using a config overrides prod. - await ng('build', '--configuration=three'); - await expectFileToExist('dist/test-project/styles.js'); - await expectToFail(() => expectFileToExist('dist/test-project/styles.css')); - // Use two configurations. - await ng('build', '--configuration=one,two', '--vendor-chunk=false'); - await expectToFail(() => expectFileToExist('dist/test-project/favicon.ico')); - await expectToFail(() => expectFileToExist('dist/test-project/main.js.map')); - // Use two configurations and two overrides, one of which overrides a config. - await ng('build', '--configuration=one,two', '--vendor-chunk=false', '--sourceMap=true'); - await expectToFail(() => expectFileToExist('dist/test-project/favicon.ico')); - await expectFileToExist('dist/test-project/main.js.map'); - await expectToFail(() => expectFileToExist('dist/test-project/vendor.js')); - // Use three configuration and check that last on value wins - await ng('build', '--configuration=one,two,three', '--vendor-chunk=false'); - await expectToFail(() => expectFileToExist('dist/test-project/favicon.ico')); - await expectToFail(() => expectFileToExist('dist/test-project/main.js.map')); - await expectToFail(() => expectFileToExist('dist/test-project/vendor.js')); - await expectFileToExist('dist/test-project/styles.js'); - await expectToFail(() => expectFileToExist('dist/test-project/styles.css')); -} diff --git a/tests/legacy-cli/e2e/tests/build/no-angular-router.ts b/tests/legacy-cli/e2e/tests/build/no-angular-router.ts deleted file mode 100644 index b5c056f6e328..000000000000 --- a/tests/legacy-cli/e2e/tests/build/no-angular-router.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {ng} from '../../utils/process'; -import {expectFileToExist, moveFile} from '../../utils/fs'; -import {getGlobalVariable} from '../../utils/env'; -import * as path from 'path'; - - -export default function() { - const tmp = getGlobalVariable('tmp-root'); - - return Promise.resolve() - .then(() => moveFile('node_modules/@angular/router', path.join(tmp, '@angular-router.backup'))) - .then(() => ng('build', '--configuration=development')) - .then(() => expectFileToExist('./dist/test-project/index.html')) - .then(() => moveFile(path.join(tmp, '@angular-router.backup'), 'node_modules/@angular/router')); -} diff --git a/tests/legacy-cli/e2e/tests/build/no-entry-module.ts b/tests/legacy-cli/e2e/tests/build/no-entry-module.ts deleted file mode 100644 index c1119b393149..000000000000 --- a/tests/legacy-cli/e2e/tests/build/no-entry-module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { readFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default async function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - const mainTs = await readFile('src/main.ts'); - - const newMainTs = mainTs - .replace(/platformBrowserDynamic.*?bootstrapModule.*?;/, '') - + 'console.log(AppModule);'; // Use AppModule to make sure it's imported properly. - - await writeFile('src/main.ts', newMainTs); - await ng('build', '--configuration=development'); -} diff --git a/tests/legacy-cli/e2e/tests/build/no-sourcemap.ts b/tests/legacy-cli/e2e/tests/build/no-sourcemap.ts deleted file mode 100644 index 61b1e22e6fa6..000000000000 --- a/tests/legacy-cli/e2e/tests/build/no-sourcemap.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as fs from 'fs'; -import { ng } from '../../utils/process'; - -export default async function () { - await ng('build', '--output-hashing=none', '--source-map', 'false'); - await testForSourceMaps(3); - - await ng('build', '--output-hashing=none', '--source-map', 'false', '--configuration=development'); - await testForSourceMaps(4); -} - -async function testForSourceMaps(expectedNumberOfFiles: number): Promise { - const files = fs.readdirSync('./dist/test-project'); - - let count = 0; - for (const file of files) { - if (!file.endsWith('.js')) { - continue; - } - - ++count; - - if (files.includes(file + '.map')) { - throw new Error('Sourcemap generated for ' + file); - } - - const content = fs.readFileSync('./dist/test-project/' + file, 'utf8'); - if (content.includes(`//# sourceMappingURL=${file}.map`)) { - throw new Error('Sourcemap comment found generated for ' + file); - } - } - - if (count < expectedNumberOfFiles) { - throw new Error(`Javascript file count is low. Expected ${expectedNumberOfFiles} but found ${count}`); - } -} diff --git a/tests/legacy-cli/e2e/tests/build/output-dir.ts b/tests/legacy-cli/e2e/tests/build/output-dir.ts deleted file mode 100644 index 43d7f5c237cb..000000000000 --- a/tests/legacy-cli/e2e/tests/build/output-dir.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {expectFileToExist} from '../../utils/fs'; -import {expectGitToBeClean} from '../../utils/git'; -import {ng} from '../../utils/process'; -import {updateJsonFile} from '../../utils/project'; -import {expectToFail} from '../../utils/utils'; - - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return ng('build', '--output-path', 'build-output', '--configuration=development') - .then(() => expectFileToExist('./build-output/index.html')) - .then(() => expectFileToExist('./build-output/main.js')) - .then(() => expectToFail(expectGitToBeClean)) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.outputPath = 'config-build-output'; - })) - .then(() => ng('build', '--configuration=development')) - .then(() => expectFileToExist('./config-build-output/index.html')) - .then(() => expectFileToExist('./config-build-output/main.js')) - .then(() => expectToFail(expectGitToBeClean)); -} diff --git a/tests/legacy-cli/e2e/tests/build/output-hashing.ts b/tests/legacy-cli/e2e/tests/build/output-hashing.ts deleted file mode 100644 index 7afa86d646b5..000000000000 --- a/tests/legacy-cli/e2e/tests/build/output-hashing.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { copyProjectAsset } from '../../utils/assets'; -import { expectFileMatchToExist, expectFileToMatch, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -async function verifyMedia(fileNameRe: RegExp, content: RegExp) { - const fileName = await expectFileMatchToExist('dist/test-project/', fileNameRe); - await expectFileToMatch(`dist/test-project/${fileName}`, content); -} - -export default async function () { - await writeMultipleFiles({ - 'src/styles.css': 'body { background-image: url("./assets/image.png"); }', - }); - // use image with file size >10KB to prevent inlining - await copyProjectAsset('images/spectrum.png', './src/assets/image.png'); - await ng('build', '--output-hashing=all', '--configuration=development'); - await expectFileToMatch('dist/test-project/index.html', /runtime\.[0-9a-f]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /main\.[0-9a-f]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /styles\.[0-9a-f]{20}\.(css|js)/); - await verifyMedia(/styles\.[0-9a-f]{20}\.(css|js)/, /image\.[0-9a-f]{20}\.png/); - - await ng('build', '--output-hashing=none', '--configuration=development'); - await expectFileToMatch('dist/test-project/index.html', /runtime\.js/); - await expectFileToMatch('dist/test-project/index.html', /main\.js/); - await expectFileToMatch('dist/test-project/index.html', /styles\.(css|js)/); - await verifyMedia(/styles\.(css|js)/, /image\.png/); - - await ng('build', '--output-hashing=media', '--configuration=development'); - await expectFileToMatch('dist/test-project/index.html', /runtime\.js/); - await expectFileToMatch('dist/test-project/index.html', /main\.js/); - await expectFileToMatch('dist/test-project/index.html', /styles\.(css|js)/); - await verifyMedia(/styles\.(css|js)/, /image\.[0-9a-f]{20}\.png/); - - await ng('build', '--output-hashing=bundles', '--configuration=development'); - await expectFileToMatch('dist/test-project/index.html', /runtime\.[0-9a-f]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /main\.[0-9a-f]{20}\.js/); - await expectFileToMatch('dist/test-project/index.html', /styles\.[0-9a-f]{20}\.(css|js)/); - await verifyMedia(/styles\.[0-9a-f]{20}\.(css|js)/, /image\.png/); -} diff --git a/tests/legacy-cli/e2e/tests/build/platform-server.ts b/tests/legacy-cli/e2e/tests/build/platform-server.ts deleted file mode 100644 index 0afb776366fb..000000000000 --- a/tests/legacy-cli/e2e/tests/build/platform-server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { normalize } from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { exec, ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -const snapshots = require('../../ng-snapshot/package.json'); - -export default async function () { - await ng('generate', 'universal', '--project', 'test-project'); - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - if (isSnapshotBuild) { - const packagesToInstall = []; - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - // Iterate over all of the packages to update them to the snapshot version. - for (const [name, version] of Object.entries(snapshots.dependencies)) { - if (name in dependencies && dependencies[name] !== version) { - packagesToInstall.push(version); - } - } - }); - - for (const pkg of packagesToInstall) { - await installPackage(pkg); - } - } - - await writeFile( - './server.ts', - ` import 'zone.js/dist/zone-node'; - import * as fs from 'fs'; - import { AppServerModule, renderModule } from './src/main.server'; - - renderModule(AppServerModule, { - url: '/', - document: '' - }).then(html => { - fs.writeFileSync('dist/test-project/server/index.html', html); - }); - `, - ); - - await replaceInFile('tsconfig.server.json', 'src/main.server.ts', 'server.ts'); - await replaceInFile('angular.json', 'src/main.server.ts', 'server.ts'); - - await ng('run', 'test-project:server', '--optimization', 'false'); - - await expectFileToMatch( - 'dist/test-project/server/main.js', - /exports.*AppServerModule|"AppServerModule":/, - ); - await exec(normalize('node'), 'dist/test-project/server/main.js'); - await expectFileToMatch( - 'dist/test-project/server/index.html', - /Here are some links to help you get started:<\/p>/, - ); - - // works with optimization and bundleDependencies enabled - await ng('run', 'test-project:server', '--optimization', '--bundleDependencies'); - await exec(normalize('node'), 'dist/test-project/server/main.js'); - await expectFileToMatch( - 'dist/test-project/server/index.html', - /Here are some links to help you get started:<\/p>/, - ); -} diff --git a/tests/legacy-cli/e2e/tests/build/poll.ts b/tests/legacy-cli/e2e/tests/build/poll.ts deleted file mode 100644 index 9a63d5edb1c2..000000000000 --- a/tests/legacy-cli/e2e/tests/build/poll.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { appendToFile } from '../../utils/fs'; -import { - killAllProcesses, - waitForAnyProcessOutputToMatch, -} from '../../utils/process'; -import { ngServe } from '../../utils/project'; -import { expectToFail, wait } from '../../utils/utils'; - -const webpackGoodRegEx = / Compiled successfully./; - -export default async function() { - try { - await ngServe('--poll=10000'); - - // Wait before editing a file. - // Editing too soon seems to trigger a rebuild and throw polling out of whack. - await wait(3000); - await appendToFile('src/main.ts', 'console.log(1);'); - - // We have to wait poll time + rebuild build time for the regex match. - await waitForAnyProcessOutputToMatch(webpackGoodRegEx, 14000); - - // No rebuilds should occur for a while - await appendToFile('src/main.ts', 'console.log(1);'); - await expectToFail(() => waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000)); - - // But a rebuild should happen roughly within the 10 second window. - await waitForAnyProcessOutputToMatch(webpackGoodRegEx, 7000); - } finally { - killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/build/polyfills.ts b/tests/legacy-cli/e2e/tests/build/polyfills.ts deleted file mode 100644 index ad94bc58a3f1..000000000000 --- a/tests/legacy-cli/e2e/tests/build/polyfills.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { - expectFileSizeToBeUnder, - expectFileToExist, - expectFileToMatch, - getFileSize, - replaceInFile, -} from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - // Enable Differential loading to run both size checks - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - await ng('build', '--aot=false', '--configuration=development'); - // files were created successfully - await expectFileToMatch('dist/test-project/polyfills-es5.js', 'core-js/proposals/reflect-metadata'); - await expectFileToMatch('dist/test-project/polyfills-es5.js', 'zone.js'); - - await expectFileToMatch('dist/test-project/index.html', oneLineTrim` - - - - - `)), - ) - // also check when css isn't extracted - .then(() => ng('build', '--no-extract-css', '--configuration=development')) - // files were created successfully - .then(() => expectFileToMatch('dist/test-project/styles.js', '.string-style')) - .then(() => expectFileToMatch('dist/test-project/styles.js', '.input-style')) - .then(() => expectFileToMatch('dist/test-project/lazy-style.js', '.lazy-style')) - .then(() => expectFileToMatch('dist/test-project/renamed-style.js', '.pre-rename-style')) - .then(() => - expectFileToMatch('dist/test-project/renamed-lazy-style.js', '.pre-rename-lazy-style'), - ) - .then(() => - expectFileToMatch( - 'dist/test-project/renamed-lazy-style.js', - '.pre-rename-lazy-style', - ), - ) - // index.html lists the right bundles - .then(() => - expectFileToMatch( - 'dist/test-project/index.html', - oneLineTrim` - - - `, - ), - ) - ); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/imports.ts b/tests/legacy-cli/e2e/tests/build/styles/imports.ts deleted file mode 100644 index 9bfb6023775b..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/imports.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - writeMultipleFiles, - expectFileToMatch, - replaceInFile -} from '../../../utils/fs'; -import { expectToFail } from '../../../utils/utils'; -import { ng } from '../../../utils/process'; -import { stripIndents } from 'common-tags'; -import { updateJsonFile } from '../../../utils/project'; -import { getGlobalVariable } from '../../../utils/env'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - const extensions = ['css', 'scss', 'less', 'styl']; - let promise = Promise.resolve(); - - extensions.forEach(ext => { - promise = promise.then(() => { - return writeMultipleFiles({ - [`src/styles.${ext}`]: stripIndents` - @import './imported-styles.${ext}'; - body { background-color: #00f; } - `, - [`src/imported-styles.${ext}`]: stripIndents` - p { background-color: #f00; } - `, - [`src/app/app.component.${ext}`]: stripIndents` - @import './imported-component-styles.${ext}'; - .outer { - .inner { - background: #fff; - } - } - `, - [`src/app/imported-component-styles.${ext}`]: stripIndents` - h1 { background: #000; } - `}) - // change files to use preprocessor - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: `src/styles.${ext}` }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - './app.component.css', `./app.component.${ext}`)) - // run build app - .then(() => ng('build', '--extract-css', '--source-map', '--configuration=development')) - // verify global styles - .then(() => expectFileToMatch('dist/test-project/styles.css', - /body\s*{\s*background-color: #00f;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /p\s*{\s*background-color: #f00;\s*}/)) - // verify global styles sourcemap - .then(() => expectToFail(() => - expectFileToMatch('dist/test-project/styles.css', '"mappings":""'))) - // verify component styles - .then(() => expectFileToMatch('dist/test-project/main.js', - /.outer.*.inner.*background:\s*#[fF]+/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /h1.*background:\s*#000+/)) - // Also check imports work on ng test - .then(() => ng('test', '--watch=false')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: `src/styles.css` }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - `./app.component.${ext}`, './app.component.css')); - }); - }); - - return promise; -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts b/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts deleted file mode 100644 index fb4cf638900a..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/include-paths.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - writeMultipleFiles, - expectFileToMatch, - replaceInFile, - createDir -} from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -export default function () { - return Promise.resolve() - .then(() => createDir('src/style-paths')) - .then(() => writeMultipleFiles({ - 'src/style-paths/_variables.scss': '$primary-color: red;', - 'src/styles.scss': ` - @import 'variables'; - h1 { color: $primary-color; } - `, - 'src/app/app.component.scss': ` - @import 'variables'; - h2 { background-color: $primary-color; } - `, - 'src/style-paths/variables.styl': '$primary-color = green', - 'src/styles.styl': ` - @import 'variables' - h3 - color: $primary-color - `, - 'src/app/app.component.styl': ` - @import 'variables' - h4 - background-color: $primary-color - `, - 'src/style-paths/variables.less': '@primary-color: #ADDADD;', - 'src/styles.less': ` - @import 'variables'; - h5 { color: @primary-color; } - `, - 'src/app/app.component.less': ` - @import 'variables'; - h6 { color: @primary-color; } - ` - })) - .then(() => replaceInFile('src/app/app.component.ts', `'./app.component.css\'`, - `'./app.component.scss', './app.component.styl', './app.component.less'`)) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' }, - { input: 'src/styles.styl' }, - { input: 'src/styles.less' }, - ]; - appArchitect.build.options.stylePreprocessorOptions = { - includePaths: [ - 'src/style-paths' - ] - }; - })) - // files were created successfully - .then(() => ng('build', '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/)) - .then(() => ng('build', '--extract-css', '--aot', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h1\s*{\s*color: red;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h2.*{.*color: red;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h3\s*{\s*color: #008000;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h4.*{.*color: #008000;.*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', /h5\s*{\s*color: #ADDADD;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/main.js', /h6.*{.*color: #ADDADD;.*}/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/less.ts b/tests/legacy-cli/e2e/tests/build/styles/less.ts deleted file mode 100644 index a5bde1be810d..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/less.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - writeMultipleFiles, - deleteFile, - expectFileToMatch, - replaceInFile -} from '../../../utils/fs'; -import { expectToFail } from '../../../utils/utils'; -import { ng } from '../../../utils/process'; -import { stripIndents } from 'common-tags'; -import { updateJsonFile } from '../../../utils/project'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return writeMultipleFiles({ - 'src/styles.less': stripIndents` - @import './imported-styles.less'; - body { background-color: blue; } - `, - 'src/imported-styles.less': stripIndents` - p { background-color: red; } - `, - 'src/app/app.component.less': stripIndents` - .outer { - .inner { - background: #fff; - } - } - `}) - .then(() => deleteFile('src/app/app.component.css')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.less' }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - './app.component.css', './app.component.less')) - .then(() => ng('build', '--extract-css', '--source-map', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /body\s*{\s*background-color: blue;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /p\s*{\s*background-color: red;\s*}/)) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/styles.css', '"mappings":""'))) - .then(() => expectFileToMatch('dist/test-project/main.js', /.outer.*.inner.*background:\s*#[fF]+/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/loaders.ts b/tests/legacy-cli/e2e/tests/build/styles/loaders.ts deleted file mode 100644 index aeed8399201c..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/loaders.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - writeMultipleFiles, - deleteFile, - expectFileToMatch, - replaceInFile -} from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { stripIndents } from 'common-tags'; -import { updateJsonFile } from '../../../utils/project'; -import { expectToFail } from '../../../utils/utils'; - -export default function () { - return writeMultipleFiles({ - 'src/styles.scss': stripIndents` - @import './imported-styles.scss'; - body { background-color: blue; } - `, - 'src/imported-styles.scss': stripIndents` - p { background-color: red; } - `, - 'src/app/app.component.scss': stripIndents` - .outer { - .inner { - background: #fff; - } - } - `}) - .then(() => deleteFile('src/app/app.component.css')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - './app.component.css', './app.component.scss')) - .then(() => ng('build', '--configuration=development')) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/styles.css', /exports/))) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/main-es5.js', - /".*module\.exports.*\.outer.*background:/))); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/material-import.ts b/tests/legacy-cli/e2e/tests/build/styles/material-import.ts deleted file mode 100644 index 00b7193522ec..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/material-import.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { stripIndents } from 'common-tags'; -import { getGlobalVariable } from '../../../utils/env'; -import { - replaceInFile, - writeMultipleFiles, -} from '../../../utils/fs'; -import { installWorkspacePackages } from '../../../utils/packages'; -import { ng } from '../../../utils/process'; -import { isPrereleaseCli, updateJsonFile } from '../../../utils/project'; - -const snapshots = require('../../../ng-snapshot/package.json'); - -export default async function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - const tag = await isPrereleaseCli() ? 'next' : 'latest'; - - await updateJsonFile('package.json', packageJson => { - const dependencies = packageJson['dependencies']; - dependencies['@angular/material'] = isSnapshotBuild ? snapshots.dependencies['@angular/material'] : tag; - dependencies['@angular/cdk'] = isSnapshotBuild ? snapshots.dependencies['@angular/cdk'] : tag; - }); - - await installWorkspacePackages(); - - for (const ext of ['css', 'scss', 'less', 'styl']) { - await writeMultipleFiles({ - [`src/styles.${ext}`]: stripIndents` - @import "~@angular/material/prebuilt-themes/indigo-pink.css"; - `, - [`src/app/app.component.${ext}`]: stripIndents` - @import "~@angular/material/prebuilt-themes/indigo-pink.css"; - `, - }); - - // change files to use preprocessor - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: `src/styles.${ext}` }, - ]; - }); - - await replaceInFile('src/app/app.component.ts', './app.component.css', `./app.component.${ext}`); - - // run build app - await ng('build', '--extract-css', '--source-map', '--configuration=development'); - await writeMultipleFiles({ - [`src/styles.${ext}`]: stripIndents` - @import "@angular/material/prebuilt-themes/indigo-pink.css"; - `, - [`src/app/app.component.${ext}`]: stripIndents` - @import "@angular/material/prebuilt-themes/indigo-pink.css"; - `, - }); - - await ng('build', '--extract-css', '--configuration=development'); - } -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/node-sass.ts b/tests/legacy-cli/e2e/tests/build/styles/node-sass.ts deleted file mode 100644 index b6d8a827e58f..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/node-sass.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - deleteFile, - expectFileToMatch, - replaceInFile, - writeMultipleFiles, -} from '../../../utils/fs'; -import { installPackage } from '../../../utils/packages'; -import { ng, silentExec } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; -import { expectToFail } from '../../../utils/utils'; - - -export default async function () { - if (process.platform.startsWith('win')) { - return; - } - - await writeMultipleFiles({ - 'src/styles.scss': '@import \'./imported-styles.scss\';\nbody { background-color: blue; }', - 'src/imported-styles.scss': 'p { background-color: red; }', - 'src/app/app.component.scss': '.outer { .inner { background: #fff; } }', - }); - await deleteFile('src/app/app.component.css'); - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' }, - ]; - }); - await replaceInFile('src/app/app.component.ts', './app.component.css', './app.component.scss'); - - await silentExec('rm', '-rf', 'node_modules/node-sass'); - await silentExec('rm', '-rf', 'node_modules/sass'); - await expectToFail(() => ng('build', '--extract-css', '--source-map', '--configuration=development')); - - await installPackage('node-sass'); - await silentExec('rm', '-rf', 'node_modules/sass'); - await ng('build', '--extract-css', '--source-map', '--configuration=development'); - - await expectFileToMatch('dist/test-project/styles.css', /body\s*{\s*background-color: blue;\s*}/); - await expectFileToMatch('dist/test-project/styles.css', /p\s*{\s*background-color: red;\s*}/); - await expectToFail(() => expectFileToMatch('dist/test-project/styles.css', '"mappings":""')); - await expectFileToMatch('dist/test-project/main.js', /.outer.*.inner.*background:\s*#[fF]+/); - - await installPackage('node-gyp'); - await installPackage('fibers'); - await installPackage('sass'); - await silentExec('rm', '-rf', 'node_modules/node-sass'); - await ng('build', '--extract-css', '--source-map', '--configuration=development'); - - await expectFileToMatch('dist/test-project/styles.css', /body\s*{\s*background-color: blue;\s*}/); - await expectFileToMatch('dist/test-project/styles.css', /p\s*{\s*background-color: red;\s*}/); - await expectToFail(() => expectFileToMatch('dist/test-project/styles.css', '"mappings":""')); - await expectFileToMatch('dist/test-project/main.js', /.outer.*.inner.*background:\s*#[fF]+/); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/preset-env.ts b/tests/legacy-cli/e2e/tests/build/styles/preset-env.ts deleted file mode 100644 index f169529e4f08..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/preset-env.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expectFileToMatch, replaceInFile, writeMultipleFiles } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; - -export default async function () { - await writeMultipleFiles({ - 'src/styles.css': `a { - all: initial; - }`, - }); - - // Enable IE 11 support - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - await ng('build', '--configuration=development'); - await expectFileToMatch('dist/test-project/styles.css', 'z-index: auto'); - await expectFileToMatch('dist/test-project/styles.css', 'all: initial'); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/scss.ts b/tests/legacy-cli/e2e/tests/build/styles/scss.ts deleted file mode 100644 index 34b5881c976f..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/scss.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - writeMultipleFiles, - deleteFile, - expectFileToMatch, - replaceInFile -} from '../../../utils/fs'; -import { expectToFail } from '../../../utils/utils'; -import { ng } from '../../../utils/process'; -import { stripIndents } from 'common-tags'; -import { updateJsonFile } from '../../../utils/project'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return writeMultipleFiles({ - 'src/styles.scss': stripIndents` - @import './imported-styles.scss'; - body { background-color: blue; } - `, - 'src/imported-styles.scss': stripIndents` - p { background-color: red; } - `, - 'src/app/app.component.scss': stripIndents` - .outer { - .inner { - background: #fff; - } - } - `}) - .then(() => deleteFile('src/app/app.component.css')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - './app.component.css', './app.component.scss')) - .then(() => ng('build', '--extract-css', '--source-map', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /body\s*{\s*background-color: blue;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /p\s*{\s*background-color: red;\s*}/)) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/styles.css', '"mappings":""'))) - .then(() => expectFileToMatch('dist/test-project/main.js', /.outer.*.inner.*background:\s*#[fF]+/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/stylus.ts b/tests/legacy-cli/e2e/tests/build/styles/stylus.ts deleted file mode 100644 index ae05f7048180..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/stylus.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - writeMultipleFiles, - deleteFile, - expectFileToMatch, - replaceInFile -} from '../../../utils/fs'; -import { expectToFail } from '../../../utils/utils'; -import { ng } from '../../../utils/process'; -import { stripIndents } from 'common-tags'; -import { updateJsonFile } from '../../../utils/project'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return writeMultipleFiles({ - 'src/styles.styl': stripIndents` - @import './imported-styles.styl'; - body { background-color: blue; } - `, - 'src/imported-styles.styl': stripIndents` - p { background-color: red; } - `, - 'src/app/app.component.styl': stripIndents` - .outer { - .inner { - background: #fff; - } - } - `}) - .then(() => deleteFile('src/app/app.component.css')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'src/styles.styl' }, - ]; - })) - .then(() => replaceInFile('src/app/app.component.ts', - './app.component.css', './app.component.styl')) - .then(() => ng('build', '--extract-css', '--source-map', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /body\s*{\s*background-color: #00f;\s*}/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /p\s*{\s*background-color: #f00;\s*}/)) - .then(() => expectToFail(() => expectFileToMatch('dist/test-project/styles.css', '"mappings":""'))) - .then(() => expectFileToMatch('dist/test-project/main.js', /.outer.*.inner.*background:\s*#[fF]+/)); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/symlinked-global.ts b/tests/legacy-cli/e2e/tests/build/styles/symlinked-global.ts deleted file mode 100644 index c0b92b7cb102..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/symlinked-global.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { symlinkSync } from 'fs'; -import { resolve } from 'path'; -import { expectFileToMatch, writeMultipleFiles } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -export default async function () { - await writeMultipleFiles({ - 'src/styles.scss': `p { color: red }`, - 'src/styles-for-link.scss': `p { color: blue }`, - }); - - symlinkSync( - resolve('src/styles-for-link.scss'), - resolve('src/styles-linked.scss'), - ); - - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - 'src/styles.scss', - 'src/styles-linked.scss', - ]; - }); - - await ng('build', '--configuration=development'); - await expectFileToMatch('dist/test-project/styles.css', 'red'); - await expectFileToMatch('dist/test-project/styles.css', 'blue'); - - await ng('build', '--preserve-symlinks', '--configuration=development'); - await expectFileToMatch('dist/test-project/styles.css', 'red'); - await expectFileToMatch('dist/test-project/styles.css', 'blue'); -} diff --git a/tests/legacy-cli/e2e/tests/build/styles/tailwind.ts b/tests/legacy-cli/e2e/tests/build/styles/tailwind.ts deleted file mode 100644 index 51f317533384..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/tailwind.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { deleteFile, expectFileToMatch, writeFile } from '../../../utils/fs'; -import { installPackage, uninstallPackage } from '../../../utils/packages'; -import { ng, silentExec } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - -export default async function () { - // Tailwind is not supported in Node.js 10 - if (process.version.startsWith('v10')) { - return; - } - - // Install Tailwind - await installPackage('tailwindcss'); - - // Create configuration file - await silentExec('npx', 'tailwindcss', 'init'); - - // Add Tailwind directives to a component style - await writeFile('src/app/app.component.css', '@tailwind base; @tailwind components;'); - - // Add Tailwind directives to a global style - await writeFile('src/styles.css', '@tailwind base; @tailwind components;'); - - // Build should succeed and process Tailwind directives - await ng('build', '--configuration=development'); - - // Check for Tailwind output - await expectFileToMatch('dist/test-project/styles.css', /::placeholder/); - await expectFileToMatch('dist/test-project/main.js', /::placeholder/); - await expectToFail(() => - expectFileToMatch('dist/test-project/styles.css', '@tailwind base; @tailwind components;'), - ); - await expectToFail(() => - expectFileToMatch('dist/test-project/main.js', '@tailwind base; @tailwind components;'), - ); - - // Remove configuration file - await deleteFile('tailwind.config.js'); - - // Ensure Tailwind is disabled when no configuration file is present - await ng('build', '--configuration=development'); - await expectFileToMatch('dist/test-project/styles.css', '@tailwind base; @tailwind components;'); - await expectFileToMatch('dist/test-project/main.js', '@tailwind base; @tailwind components;'); - - // Recreate configuration file - await silentExec('npx', 'tailwindcss', 'init'); - - // Uninstall Tailwind - await uninstallPackage('tailwindcss'); - - // Ensure installation warning is present - const { stderr } = await ng('build', '--configuration=development'); - if (!stderr.includes("To enable Tailwind CSS, please install the 'tailwindcss' package.")) { - throw new Error('Expected tailwind installation warning'); - } - - // Tailwind directives should be unprocessed with missing package - await expectFileToMatch('dist/test-project/styles.css', '@tailwind base; @tailwind components;'); - await expectFileToMatch('dist/test-project/main.js', '@tailwind base; @tailwind components;'); -} diff --git a/tests/legacy-cli/e2e/tests/build/subresource-integrity.ts b/tests/legacy-cli/e2e/tests/build/subresource-integrity.ts deleted file mode 100644 index 6592fd373154..000000000000 --- a/tests/legacy-cli/e2e/tests/build/subresource-integrity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -const integrityRe = /integrity="\w+-[A-Za-z0-9\/\+=]+"/; - -export default async function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - // WEBPACK4_DISABLED - disabled pending a webpack 4 version - return; - - return ng('build') - .then(() => expectToFail(() => - expectFileToMatch('dist/test-project/index.html', integrityRe))) - .then(() => ng('build', '--sri')) - .then(() => expectFileToMatch('dist/test-project/index.html', integrityRe)); -} diff --git a/tests/legacy-cli/e2e/tests/build/ts-paths.ts b/tests/legacy-cli/e2e/tests/build/ts-paths.ts deleted file mode 100644 index 10510a9ee6a6..000000000000 --- a/tests/legacy-cli/e2e/tests/build/ts-paths.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { stripIndents } from 'common-tags'; -import { appendToFile, createDir, replaceInFile, rimraf, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateTsConfig } from '../../utils/project'; - -export default async function () { - await updateTsConfig(json => { - json['compilerOptions']['baseUrl'] = './src'; - json['compilerOptions']['paths'] = { - '@shared': [ - 'app/shared', - ], - '@shared/*': [ - 'app/shared/*', - ], - '@root/*': [ - './*', - ], - }; - }); - - await createDir('src/app/shared'); - await writeMultipleFiles({ - 'src/meaning-too.ts': 'export var meaning = 42;', - 'src/app/shared/meaning.ts': 'export var meaning = 42;', - 'src/app/shared/index.ts': `export * from './meaning'`, - }); - - await replaceInFile('src/app/app.module.ts', './app.component', '@root/app/app.component'); - await ng('build', '--configuration=development'); - - await updateTsConfig(json => { - json['compilerOptions']['paths']['*'] = [ - '*', - 'app/shared/*', - ]; - }); - - await appendToFile('src/app/app.component.ts', stripIndents` - import { meaning } from 'app/shared/meaning'; - import { meaning as meaning2 } from '@shared'; - import { meaning as meaning3 } from '@shared/meaning'; - import { meaning as meaning4 } from 'meaning'; - import { meaning as meaning5 } from 'meaning-too'; - - // need to use imports otherwise they are ignored and - // no error is outputted, even if baseUrl/paths don't work - console.log(meaning) - console.log(meaning2) - console.log(meaning3) - console.log(meaning4) - console.log(meaning5) - `); - - await ng('build', '--configuration=development'); - - // Simulate no package.json file which causes Webpack to have an undefined 'descriptionFileData'. - await rimraf('package.json'); - await ng('build', '--configuration=development'); -} diff --git a/tests/legacy-cli/e2e/tests/build/vendor-chunk.ts b/tests/legacy-cli/e2e/tests/build/vendor-chunk.ts deleted file mode 100644 index 02c82a86d7e2..000000000000 --- a/tests/legacy-cli/e2e/tests/build/vendor-chunk.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expectFileToExist } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - - -export default async function () { - await ng('build', '--configuration=development'); - await expectFileToExist('dist/test-project/vendor.js'); - await ng('build', '--configuration=development', '--vendor-chunk=false'); - await expectToFail(() => expectFileToExist('dist/test-project/vendor.js')); -} diff --git a/tests/legacy-cli/e2e/tests/build/worker.ts b/tests/legacy-cli/e2e/tests/build/worker.ts deleted file mode 100644 index 426890f1d2fe..000000000000 --- a/tests/legacy-cli/e2e/tests/build/worker.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { join } from 'path'; -import { expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - const workerPath = join('src', 'app', 'app.worker.ts'); - const snippetPath = join('src', 'app', 'app.component.ts'); - const projectTsConfig = 'tsconfig.json'; - const workerTsConfig = 'tsconfig.worker.json'; - - // Enable Differential loading to run both size checks - await replaceInFile('.browserslistrc', 'not IE 11', 'IE 11'); - - await ng('generate', 'web-worker', 'app'); - await expectFileToExist(workerPath); - await expectFileToExist(projectTsConfig); - await expectFileToExist(workerTsConfig); - await expectFileToMatch(snippetPath, `new Worker(new URL('./app.worker', import.meta.url)`); - - await ng('build', '--configuration=development'); - await expectFileToExist('dist/test-project/src_app_app_worker_ts-es5.js'); - await expectFileToMatch('dist/test-project/main-es5.js', 'src_app_app_worker_ts'); - await expectFileToExist('dist/test-project/src_app_app_worker_ts-es2017.js'); - await expectFileToMatch('dist/test-project/main-es2017.js', 'src_app_app_worker_ts'); - - await ng('build', '--output-hashing=none'); - const chunkId = '151'; - await expectFileToExist(`dist/test-project/${chunkId}-es5.js`); - await expectFileToMatch('dist/test-project/main-es5.js', chunkId); - await expectFileToExist(`dist/test-project/${chunkId}-es2017.js`); - await expectFileToMatch('dist/test-project/main-es2017.js', chunkId); - - // console.warn has to be used because chrome only captures warnings and errors by default - // https://github.com/angular/protractor/issues/2207 - await replaceInFile('src/app/app.component.ts', 'console.log', 'console.warn'); - - await writeFile( - 'e2e/app.e2e-spec.ts', - ` - import { AppPage } from './app.po'; - import { browser, logging } from 'protractor'; - describe('worker bundle', () => { - it('should log worker messages', async () => { - const page = new AppPage();; - page.navigateTo(); - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs.length).toEqual(1); - expect(logs[0].message).toContain('page got message: worker response to hello'); - }); - }); - `, - ); - - await ng('e2e'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts b/tests/legacy-cli/e2e/tests/commands/add/add-material.ts deleted file mode 100644 index 39496611be33..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/add-material.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expectFileToMatch, rimraf } from '../../../utils/fs'; -import { uninstallPackage } from '../../../utils/packages'; -import { ng } from '../../../utils/process'; -import { isPrereleaseCli } from '../../../utils/project'; - - -export default async function () { - // forcibly remove in case another test doesn't clean itself up - await rimraf('node_modules/@angular/material'); - - const tag = await isPrereleaseCli() ? '@next' : ''; - - try { - await ng('add', `@angular/material${tag}`, '--unknown', '--skip-confirmation'); - } catch (error) { - if (!(error.message && error.message.includes(`Unknown option: '--unknown'`))) { - throw error; - } - } - - await ng('add', `@angular/material${tag}`, '--theme', 'custom', '--verbose', '--skip-confirmation'); - await expectFileToMatch('package.json', /@angular\/material/); - - // Clean up existing cdk package - // Not doing so can cause adding material to fail if an incompatible cdk is present - await uninstallPackage('@angular/cdk'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts b/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts deleted file mode 100644 index 48dd9f29bc1c..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/add-pwa.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { join } from 'path'; -import { getGlobalVariable } from '../../../utils/env'; -import { expectFileToExist, readFile, rimraf } from '../../../utils/fs'; -import { installWorkspacePackages } from '../../../utils/packages'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -const snapshots = require('../../../ng-snapshot/package.json'); - -export default async function () { - // forcibly remove in case another test doesn't clean itself up - await rimraf('node_modules/@angular/pwa'); - await ng('add', '@angular/pwa', '--skip-confirmation'); - await expectFileToExist(join(process.cwd(), 'src/manifest.webmanifest')); - - // Angular PWA doesn't install as a dependency - const { dependencies, devDependencies } = JSON.parse( - await readFile(join(process.cwd(), 'package.json')), - ); - const hasPWADep = Object.keys({ ...dependencies, ...devDependencies }).some( - (d) => d === '@angular/pwa', - ); - if (hasPWADep) { - throw new Error(`Expected 'package.json' not to contain a dependency on '@angular/pwa'.`); - } - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - if (isSnapshotBuild) { - let needInstall = false; - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - // Iterate over all of the packages to update them to the snapshot version. - for (const [name, version] of Object.entries(snapshots.dependencies)) { - if (name in dependencies && dependencies[name] !== version) { - dependencies[name] = version; - needInstall = true; - } - } - }); - - if (needInstall) { - await installWorkspacePackages(); - } - } - - // It should generate a SW configuration file (`ngsw.json`). - const workspaceJson = JSON.parse(await readFile('angular.json')); - const outputPath = workspaceJson.projects['test-project'].architect.build.options.outputPath; - const ngswPath = join(process.cwd(), outputPath, 'ngsw.json'); - - await ng('build'); - await expectFileToExist(ngswPath); - - // It should correctly generate assetGroups and include at least one URL in each group. - const ngswJson = JSON.parse(await readFile(ngswPath)); - const assetGroups = ngswJson.assetGroups.map(({ name, urls }) => ({ - name, - urlCount: urls.length, - })); - const emptyAssetGroups = assetGroups.filter(({ urlCount }) => urlCount === 0); - - if (assetGroups.length === 0) { - throw new Error("Expected 'ngsw.json' to contain at least one asset-group."); - } - if (emptyAssetGroups.length > 0) { - throw new Error( - 'Expected all asset-groups to contain at least one URL, but the following groups are empty: ' + - emptyAssetGroups.map(({ name }) => name).join(', '), - ); - } -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/add.ts b/tests/legacy-cli/e2e/tests/commands/add/add.ts deleted file mode 100644 index bf3ae5e3d0b5..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/add.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expectFileToExist, expectFileToMatch, rimraf } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; - - -export default async function () { - await ng('add', '@angular-devkit-tests/ng-add-simple', '--skip-confirmation'); - await expectFileToMatch('package.json', /@angular-devkit-tests\/ng-add-simple/); - await expectFileToExist('ng-add-test'); - await rimraf('node_modules/@angular-devkit-tests/ng-add-simple'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/base.ts b/tests/legacy-cli/e2e/tests/commands/add/base.ts deleted file mode 100644 index b22583909076..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/base.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { assetDir } from '../../../utils/assets'; -import { expectFileToExist, rimraf, symlinkFile } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - - -export default async function () { - await symlinkFile(assetDir('add-collection'), `./node_modules/add-collection`, 'dir'); - - await ng('add', 'add-collection'); - await expectFileToExist('empty-file'); - - await ng('add', 'add-collection', '--name=blah'); - await expectFileToExist('blah'); - - await expectToFail(() => ng('add', 'add-collection')); // File already exists. - - // Cleanup the package - await rimraf('node_modules/add-collection'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/dir.ts b/tests/legacy-cli/e2e/tests/commands/add/dir.ts deleted file mode 100644 index 695c9369a43b..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/dir.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assetDir } from '../../../utils/assets'; -import { expectFileToExist } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; - - -export default async function () { - await ng('add', assetDir('add-collection'), '--name=blah', '--skip-confirmation'); - await expectFileToExist('blah'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/file.ts b/tests/legacy-cli/e2e/tests/commands/add/file.ts deleted file mode 100644 index d05abdb9e0f3..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/file.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { assetDir } from '../../../utils/assets'; -import { expectFileToExist } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; - - -export default async function () { - await ng('add', assetDir('add-collection.tgz'), '--name=blah', '--skip-confirmation'); - await expectFileToExist('blah'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/peer.ts b/tests/legacy-cli/e2e/tests/commands/add/peer.ts deleted file mode 100644 index 2f5147df0b03..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/peer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { assetDir } from '../../../utils/assets'; -import { ng } from '../../../utils/process'; - -const warning = 'Adding the package may not succeed.'; - -export default async function () { - const { stderr: bad } = await ng('add', assetDir('add-collection-peer-bad'), '--skip-confirmation'); - if (!bad.includes(warning)) { - throw new Error('peer warning not shown on bad package'); - } - - const { stderr: base } = await ng('add', assetDir('add-collection'), '--skip-confirmation'); - if (base.includes(warning)) { - throw new Error('peer warning shown on base package'); - } - - const { stderr: good } = await ng('add', assetDir('add-collection-peer-good'), '--skip-confirmation'); - if (good.includes(warning)) { - throw new Error('peer warning shown on good package'); - } -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts b/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts deleted file mode 100644 index 8a9e31dcb4d6..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/registry-option.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getGlobalVariable } from '../../../utils/env'; -import { expectFileToExist, writeMultipleFiles } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - -export default async function () { - const testRegistry = getGlobalVariable('package-registry'); - - // Setup an invalid registry - await writeMultipleFiles({ - '.npmrc': 'registry=http://127.0.0.1:9999', - }); - // The environment variable has priority over the .npmrc - const originalRegistryVariable = process.env['NPM_CONFIG_REGISTRY']; - process.env['NPM_CONFIG_REGISTRY'] = undefined; - - try { - await expectToFail(() => ng('add', '@angular/pwa', '--skip-confirmation')); - - await ng('add', `--registry=${testRegistry}`, '@angular/pwa', '--skip-confirmation'); - await expectFileToExist('src/manifest.webmanifest'); - } finally { - process.env['NPM_CONFIG_REGISTRY'] = originalRegistryVariable; - } -} diff --git a/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts b/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts deleted file mode 100644 index f7c8cdd9f820..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/add/version-specifier.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expectFileToMatch, rimraf } from '../../../utils/fs'; -import { uninstallPackage } from '../../../utils/packages'; -import { ng } from '../../../utils/process'; -import { isPrereleaseCli } from '../../../utils/project'; - - -export default async function () { - // forcibly remove in case another test doesn't clean itself up. - await rimraf('node_modules/@angular/localize'); - - const tag = await isPrereleaseCli() ? '@next' : ''; - - await ng('add', `@angular/localize${tag}`, '--skip-confirmation'); - await expectFileToMatch('package.json', /@angular\/localize/); - - const output1 = await ng('add', '@angular/localize', '--skip-confirmation'); - if (!output1.stdout.includes('Skipping installation: Package already installed')) { - throw new Error('Installation was not skipped'); - } - - const output2 = await ng('add', '@angular/localize@latest', '--skip-confirmation'); - if (output2.stdout.includes('Skipping installation: Package already installed')) { - throw new Error('Installation should not have been skipped'); - } - - const output3 = await ng('add', '@angular/localize@10.0.0', '--skip-confirmation'); - if (output3.stdout.includes('Skipping installation: Package already installed')) { - throw new Error('Installation should not have been skipped'); - } - - const output4 = await ng('add', '@angular/localize@10', '--skip-confirmation'); - if (!output4.stdout.includes('Skipping installation: Package already installed')) { - throw new Error('Installation was not skipped'); - } - - await uninstallPackage('@angular/localize'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/additional-properties.ts b/tests/legacy-cli/e2e/tests/commands/additional-properties.ts deleted file mode 100644 index b9a477c7cff6..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/additional-properties.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createDir, rimraf, writeMultipleFiles } from '../../utils/fs'; -import { execAndWaitForOutputToMatch } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function() { - await createDir('example-builder'); - await writeMultipleFiles({ - 'example-builder/package.json': '{ "builders": "./builders.json" }', - 'example-builder/schema.json': '{ "$schema": "http://json-schema.org/draft-07/schema", "type": "object", "additionalProperties": true }', - 'example-builder/builders.json': '{ "$schema": "@angular-devkit/architect/src/builders-schema.json", "builders": { "example": { "implementation": "./example", "schema": "./schema.json" } } }', - 'example-builder/example.js': 'module.exports.default = require("@angular-devkit/architect").createBuilder((options) => { console.log(options); return { success: true }; });', - }); - - await updateJsonFile('angular.json', json => { - const appArchitect = json.projects['test-project'].architect; - appArchitect.example = { - builder: './example-builder:example', - }; - }); - - await execAndWaitForOutputToMatch( - 'ng', - ['run', 'test-project:example', '--additional', 'property'], - /'{ '--': \[ '--additional', 'property' \] }'/, - ); - - await rimraf('example-builder'); -} diff --git a/tests/legacy-cli/e2e/tests/commands/build/build-outdir.ts b/tests/legacy-cli/e2e/tests/commands/build/build-outdir.ts deleted file mode 100644 index 6cfc60afa39a..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/build/build-outdir.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {ng} from '../../../utils/process'; -import {updateJsonFile} from '../../../utils/project'; -import {expectToFail} from '../../../utils/utils'; - -export default function() { - // TODO(architect): This isn't working correctly in devkit/build-angular, due to module resolution. - return; - - return Promise.resolve() - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.outputPath = './'; - })) - .then(() => expectToFail(() => ng('build', '--configuration=development'))) - .then(() => expectToFail(() => ng('serve'))); -} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-get.ts b/tests/legacy-cli/e2e/tests/commands/config/config-get.ts deleted file mode 100644 index 4786838afefc..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/config/config-get.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle'))) - .then(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'false')) - .then(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle')) - .then(({ stdout }) => { - if (!stdout.match(/false\n?/)) { - throw new Error(`Expected "false", received "${JSON.stringify(stdout)}".`); - } - }) - .then(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'true')) - .then(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle')) - .then(({ stdout }) => { - if (!stdout.match(/true\n?/)) { - throw new Error(`Expected "true", received "${JSON.stringify(stdout)}".`); - } - }) - .then(() => ng('config', 'schematics.@schematics/angular.component.inlineStyle', 'false')) - .then(() => ng('config', `projects.test-project.architect.build.options.assets[0]`)) - .then(({ stdout }) => { - if (!stdout.includes('src/favicon.ico')) { - throw new Error(`Expected "src/favicon.ico", received "${JSON.stringify(stdout)}".`); - } - }) - .then(() => ng('config', `projects["test-project"].architect.build.options.assets[0]`)) - .then(({ stdout }) => { - if (!stdout.includes('src/favicon.ico')) { - throw new Error(`Expected "src/favicon.ico", received "${JSON.stringify(stdout)}".`); - } - }); -} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-global.ts b/tests/legacy-cli/e2e/tests/commands/config/config-global.ts deleted file mode 100644 index 8327836fc500..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/config/config-global.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { homedir } from 'os'; -import * as path from 'path'; -import { deleteFile, expectFileToExist } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - - -export default async function() { - await expectToFail(() => ng( - 'config', - '--global', - 'schematics.@schematics/angular.component.inlineStyle', - )); - - await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'false'); - let output = await ng( - 'config', - '--global', - 'schematics.@schematics/angular.component.inlineStyle', - ); - if (!output.stdout.match(/false\n?/)) { - throw new Error(`Expected "false", received "${JSON.stringify(output.stdout)}".`); - } - - // This test requires schema querying capabilities - // .then(() => expectToFail(() => { - // return ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'INVALID_BOOLEAN'); - // })) - - const cwd = process.cwd(); - process.chdir('/'); - try { - await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle', 'true'); - } finally { - process.chdir(cwd); - } - - output = await ng('config', '--global', 'schematics.@schematics/angular.component.inlineStyle'); - if (!output.stdout.match(/true\n?/)) { - throw new Error(`Expected "true", received "${JSON.stringify(output.stdout)}".`); - } - - await expectToFail(() => ng('config', '--global', 'cli.warnings.notreal', 'true')); - - await ng('config', '--global', 'cli.warnings.versionMismatch', 'false'); - await expectFileToExist(path.join(homedir(), '.angular-config.json')); - await deleteFile(path.join(homedir(), '.angular-config.json')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-set-enum-check.ts b/tests/legacy-cli/e2e/tests/commands/config/config-set-enum-check.ts deleted file mode 100644 index 694f90f6020a..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/config/config-set-enum-check.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ng } from '../../../utils/process'; - -export default async function() { - - // These tests require schema querying capabilities - // .then(() => expectToFail( - // () => ng('config', 'schematics.@schematics/angular.component.aaa', 'bbb')), - // ) - // .then(() => expectToFail(() => ng( - // 'config', - // 'schematics.@schematics/angular.component.viewEncapsulation', - // 'bbb', - // ))) - - await ng( - 'config', - 'schematics.@schematics/angular.component.viewEncapsulation', - 'Emulated', - ); -} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-set-prefix.ts b/tests/legacy-cli/e2e/tests/commands/config/config-set-prefix.ts deleted file mode 100644 index 904050649129..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/config/config-set-prefix.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => ng('config', 'schematics.@schematics/angular.component.prefix'))) - .then(() => ng('config', 'schematics.@schematics/angular.component.prefix' , 'new-prefix')) - .then(() => ng('config', 'schematics.@schematics/angular.component.prefix')) - .then(({ stdout }) => { - if (!stdout.match(/new-prefix/)) { - throw new Error(`Expected "new-prefix", received "${JSON.stringify(stdout)}".`); - } - }); -} diff --git a/tests/legacy-cli/e2e/tests/commands/config/config-set.ts b/tests/legacy-cli/e2e/tests/commands/config/config-set.ts deleted file mode 100644 index 26bf399763a6..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/config/config-set.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - -export default async function () { - await expectToFail(() => ng('config', 'cli.warnings.zzzz')); - await ng('config', 'cli.warnings.versionMismatch', 'false'); - const { stdout } = await ng('config', 'cli.warnings.versionMismatch'); - if (!stdout.includes('false')) { - throw new Error(`Expected "false", received "${JSON.stringify(stdout)}".`); - } - - await ng('config', 'cli.packageManager', 'yarn'); - const { stdout: stdout2 } = await ng('config', 'cli.packageManager'); - if (!stdout2.includes('yarn')) { - throw new Error(`Expected "yarn", received "${JSON.stringify(stdout2)}".`); - } - - await ng('config', 'schematics', '{"@schematics/angular:component":{"style": "scss"}}'); - const { stdout: stdout3 } = await ng('config', 'schematics.@schematics/angular:component.style'); - if (!stdout3.includes('scss')) { - throw new Error(`Expected "scss", received "${JSON.stringify(stdout3)}".`); - } - - await ng('config', 'schematics'); - await ng('config', 'schematics', 'undefined'); - await expectToFail(() => ng('config', 'schematics')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts b/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts deleted file mode 100644 index 5f88a787257c..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-hidden.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { oneLine } from 'common-tags'; - -import { silentNg } from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('--help')) - .then(({ stdout }) => { - if (stdout.match(/(easter-egg)|(ng make-this-awesome)|(ng init)/)) { - throw new Error(oneLine` - Expected to not match "(easter-egg)|(ng make-this-awesome)|(ng init)" - in help output. - `); - } - }) - .then(() => silentNg('--help', 'new')) - .then(({ stdout }) => { - if (stdout.match(/--link-cli/)) { - throw new Error(oneLine` - Expected to not match "--link-cli" - in help output. - `); - } - }) -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts deleted file mode 100644 index 898adfbe5bc6..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { silentNg } from '../../../utils/process'; - - -export default async function() { - const commands = require('@angular/cli/commands.json'); - for (const commandName of Object.keys(commands)) { - const { stdout } = await silentNg(commandName, '--help=json'); - - if (stdout.trim()) { - JSON.parse(stdout, (key, value) => { - if (key === 'name' && /[A-Z]/.test(value)) { - throw new Error(`Option named '${value}' is not kebab case.`); - } - }); - } else { - console.warn(`No JSON output for command [${commandName}].`); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts b/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts deleted file mode 100644 index c6782765b839..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('--help', 'build')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-option.ts b/tests/legacy-cli/e2e/tests/commands/help/help-option.ts deleted file mode 100644 index 03b96b5758d9..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-option.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('--help')) - .then(() => process.chdir('/')) - .then(() => silentNg('--help')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help.ts b/tests/legacy-cli/e2e/tests/commands/help/help.ts deleted file mode 100644 index f326f6a81ff8..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('help')) - .then(() => process.chdir('/')) - .then(() => silentNg('help')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/serve/reload-shims.ts b/tests/legacy-cli/e2e/tests/commands/serve/reload-shims.ts deleted file mode 100644 index d597ac1e39fc..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/serve/reload-shims.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { prependToFile, writeFile } from '../../../utils/fs'; -import { execAndWaitForOutputToMatch, killAllProcesses } from '../../../utils/process'; - -export default async function() { - // Simulate a JS library using a Node.js specific module - await writeFile('src/node-usage.js', `const path = require('path');\n`); - await prependToFile('src/main.ts', `import './node-usage';\n`); - - try { - // Make sure serve is consistent with build - await execAndWaitForOutputToMatch( - 'ng', - ['build'], - /Module not found: Error: Can't resolve 'path'/, - ); - // The Node.js specific module should not be found - await execAndWaitForOutputToMatch( - 'ng', - ['serve', '--port=0'], - /Module not found: Error: Can't resolve 'path'/, - ); - } finally { - killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts b/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts deleted file mode 100644 index 2075b1acd3c0..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { request } from '../../../utils/http'; -import { killAllProcesses } from '../../../utils/process'; -import { ngServe } from '../../../utils/project'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => ngServe('--serve-path', 'test/')) - .then(() => request('http://localhost:4200/test')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => request('http://localhost:4200/test/abc')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - // .then(() => ngServe('--base-href', 'test/')) - // .then(() => request('http://localhost:4200/test')) - // .then(body => { - // if (!body.match(/<\/app-root>/)) { - // throw new Error('Response does not match expected value.'); - // } - // }) - // .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); -} diff --git a/tests/legacy-cli/e2e/tests/commands/unknown-configuration.ts b/tests/legacy-cli/e2e/tests/commands/unknown-configuration.ts deleted file mode 100644 index 98257ee86e70..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/unknown-configuration.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ng } from "../../utils/process"; - -export default async function () { - try { - await ng('build', '--configuration', 'invalid'); - throw new Error('should have failed.'); - } catch (error) { - if (!error.message.includes(`Configuration 'invalid' is not set in the workspace`)) { - throw error; - } - } -}; diff --git a/tests/legacy-cli/e2e/tests/commands/unknown-option.ts b/tests/legacy-cli/e2e/tests/commands/unknown-option.ts deleted file mode 100644 index 220d74bc1646..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/unknown-option.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function() { - await expectToFail(() => ng('build', '--notanoption')); - - await execAndWaitForOutputToMatch( - 'ng', - [ 'build', '--notanoption' ], - /Unknown option: '--notanoption'/, - ); - - await expectToFail(() => execAndWaitForOutputToMatch( - 'ng', - [ 'build', '--notanoption' ], - /should NOT have additional properties\(notanoption\)./, - )); - - const ngGenerateArgs = [ 'generate', 'component', 'component-name', '--notanoption' ]; - await expectToFail(() => ng(...ngGenerateArgs)); - - await execAndWaitForOutputToMatch( - 'ng', - ngGenerateArgs, - /Unknown option: '--notanoption'/, - ); -} diff --git a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts b/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts deleted file mode 100644 index 44c92b2c347e..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/application/application-basic.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expectFileToMatch } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { useCIChrome } from '../../../utils/project'; - - -export default function() { - return ng('generate', 'application', 'app2') - .then(() => expectFileToMatch('angular.json', /\"app2\":/)) - .then(() => useCIChrome('projects/app2')) - .then(() => ng('test', 'app2', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/class.ts b/tests/legacy-cli/e2e/tests/generate/class.ts deleted file mode 100644 index aa3ba25b5899..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/class.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../utils/process'; -import {expectFileToExist} from '../../utils/fs'; - - -export default function() { - const projectDir = join('src', 'app'); - - return ng('generate', 'class', 'test-class') - .then(() => expectFileToExist(projectDir)) - .then(() => expectFileToExist(join(projectDir, 'test-class.ts'))) - .then(() => expectFileToExist(join(projectDir, 'test-class.spec.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-basic.ts b/tests/legacy-cli/e2e/tests/generate/component/component-basic.ts deleted file mode 100644 index 7c39f144d00d..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-basic.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist, expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const projectDir = join('src', 'app'); - const componentDir = join(projectDir, 'test-component'); - - const importCheck = - `import { TestComponentComponent } from './test-component/test-component.component';`; - return ng('generate', 'component', 'test-component') - .then(() => expectFileToExist(componentDir)) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.ts'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.spec.ts'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.html'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.css'))) - .then(() => expectFileToMatch(join(projectDir, 'app.module.ts'), importCheck)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-duplicate.ts b/tests/legacy-cli/e2e/tests/generate/component/component-duplicate.ts deleted file mode 100644 index 7c8e4074bb07..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-duplicate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { oneLine } from 'common-tags'; -import { appendToFile } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { expectToFail } from '../../../utils/utils'; - -export default function () { - return ng('generate', 'component', 'test-component') - .then((output) => { - if (!output.stdout.match(/UPDATE src[\\|\/]app[\\|\/]app.module.ts/)) { - throw new Error(oneLine` - Expected to match - "UPDATE src/app.module.ts" - in ${output.stdout}.`); - } - }) - .then(() => appendToFile('src/app/test-component/test-component.component.ts', '\n// new content')) - .then(() => expectToFail(() => ng('generate', 'component', 'test-component'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-flag-case.ts b/tests/legacy-cli/e2e/tests/generate/component/component-flag-case.ts deleted file mode 100644 index a759ae01832a..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-flag-case.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - // TODO:BREAKING CHANGE... NO LONGER SUPPORTED - return Promise.resolve(); - const compDir = join('projects', 'test-project', 'src', 'test'); - - return Promise.resolve() - .then(() => ng('generate', 'component', 'test', - '--change-detection', 'onpush', - '--view-encapsulation', 'emulated')) - .then(() => expectFileToMatch(join(compDir, 'test.component.ts'), - /changeDetection: ChangeDetectionStrategy.OnPush/)) - .then(() => expectFileToMatch(join(compDir, 'test.component.ts'), - /encapsulation: ViewEncapsulation.Emulated/)); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-flat.ts b/tests/legacy-cli/e2e/tests/generate/component/component-flat.ts deleted file mode 100644 index 448a6aef4df3..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-flat.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; -import {updateJsonFile} from '../../../utils/project'; - - -export default function() { - const appDir = join('src', 'app'); - return Promise.resolve() - .then(() => updateJsonFile('angular.json', configJson => { - configJson.projects['test-project'].schematics = { - '@schematics/angular:component': { flat: true } - }; - })) - .then(() => ng('generate', 'component', 'test-component')) - .then(() => expectFileToExist(appDir)) - .then(() => expectFileToExist(join(appDir, 'test-component.component.ts'))) - .then(() => expectFileToExist(join(appDir, 'test-component.component.spec.ts'))) - .then(() => expectFileToExist(join(appDir, 'test-component.component.html'))) - .then(() => expectFileToExist(join(appDir, 'test-component.component.css'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-in-existing-module-dir.ts b/tests/legacy-cli/e2e/tests/generate/component/component-in-existing-module-dir.ts deleted file mode 100644 index bb98447bb4a1..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-in-existing-module-dir.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { join } from 'path'; -import { ng } from '../../../utils/process'; -import { expectFileToMatch } from '../../../utils/fs'; - -export default function () { - const modulePath = join('src', 'app', 'foo', 'foo.module.ts'); - - return Promise.resolve() - .then(() => ng('generate', 'module', 'foo')) - .then(() => ng('generate', 'component', 'foo')) - .then(() => expectFileToMatch(modulePath, /import { FooComponent } from '.\/foo.component'/)); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-inline-template.ts b/tests/legacy-cli/e2e/tests/generate/component/component-inline-template.ts deleted file mode 100644 index f76409820a1a..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-inline-template.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; -import {updateJsonFile} from '../../../utils/project'; -import { expectToFail } from '../../../utils/utils'; - - -// tslint:disable:max-line-length -export default function() { - const componentDir = join('src', 'app', 'test-component'); - return Promise.resolve() - .then(() => updateJsonFile('angular.json', configJson => { - configJson.projects['test-project'].schematics = { - '@schematics/angular:component': { inlineTemplate: true } - }; - })) - .then(() => ng('generate', 'component', 'test-component')) - .then(() => expectFileToExist(componentDir)) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.ts'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.spec.ts'))) - .then(() => expectToFail(() => expectFileToExist(join(componentDir, 'test-component.component.html')))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.css'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-module-2.ts b/tests/legacy-cli/e2e/tests/generate/component/component-module-2.ts deleted file mode 100644 index c5af6fad4d0a..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-module-2.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const root = process.cwd(); - const modulePath = join(root, 'src', 'app', - 'admin', 'module', 'module.module.ts'); - - return Promise.resolve() - .then(() => ng('generate', 'module', 'admin/module')) - .then(() => ng('generate', 'component', 'other/test-component', '--module', 'admin/module')) - .then(() => expectFileToMatch(modulePath, - new RegExp(/import { TestComponentComponent } /.source + - /from '..\/..\/other\/test-component\/test-component.component'/.source))) - - // Try to run the unit tests. - .then(() => ng('build', '--configuration=development')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-module-export.ts b/tests/legacy-cli/e2e/tests/generate/component/component-module-export.ts deleted file mode 100644 index 4c2b2e89d8c5..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-module-export.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'component', 'test-component', '--export') - .then(() => expectFileToMatch(modulePath, /exports: \[\r?\n(\s*) TestComponentComponent\r?\n\1\]/)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-module-fail.ts b/tests/legacy-cli/e2e/tests/generate/component/component-module-fail.ts deleted file mode 100644 index f4efd1eb8ff6..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-module-fail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {ng} from '../../../utils/process'; -import {expectToFail} from '../../../utils/utils'; - - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => - ng('generate', 'component', 'test-component', '--module', 'app.moduleXXX.ts'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-module.ts b/tests/legacy-cli/e2e/tests/generate/component/component-module.ts deleted file mode 100644 index 9a77eaadea48..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const root = process.cwd(); - // projects/ test-project/ src/ app.module.ts - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'component', 'test-component', '--module', 'app.module.ts') - .then(() => expectFileToMatch(modulePath, - /import { TestComponentComponent } from '.\/test-component\/test-component.component'/)) - - .then(() => process.chdir(join(root, 'src', 'app'))) - .then(() => ng('generate', 'component', 'test-component2', '--module', 'app.module.ts')) - .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(modulePath, - /import { TestComponent2Component } from '.\/test-component2\/test-component2.component'/)) - - // Try to run the unit tests. - .then(() => ng('build', '--configuration=development')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-not-flat.ts b/tests/legacy-cli/e2e/tests/generate/component/component-not-flat.ts deleted file mode 100644 index d25936fb6e4f..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-not-flat.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; -import {updateJsonFile} from '../../../utils/project'; - - -export default function() { - const componentDir = join('src', 'app', 'test-component'); - - return Promise.resolve() - .then(() => updateJsonFile('angular.json', configJson => { - configJson.projects['test-project'].schematics = { - '@schematics/angular:component': { flat: false } - }; - })) - .then(() => ng('generate', 'component', 'test-component')) - .then(() => expectFileToExist(componentDir)) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.ts'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.spec.ts'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.html'))) - .then(() => expectFileToExist(join(componentDir, 'test-component.component.css'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/component/component-prefix.ts b/tests/legacy-cli/e2e/tests/generate/component/component-prefix.ts deleted file mode 100644 index f63dece7672a..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/component/component-prefix.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; -import { updateJsonFile } from '../../../utils/project'; - - -export default function() { - const testCompDir = join('src', 'app', 'test-component'); - const aliasCompDir = join('src', 'app', 'alias'); - - return Promise.resolve() - .then(() => updateJsonFile('angular.json', configJson => { - configJson.projects['test-project'].schematics = { - '@schematics/angular:component': { prefix: 'pre' } - }; - })) - .then(() => ng('generate', 'component', 'test-component')) - .then(() => expectFileToMatch(join(testCompDir, 'test-component.component.ts'), - /selector: 'pre-/)) - .then(() => ng('g', 'c', 'alias')) - .then(() => expectFileToMatch(join(aliasCompDir, 'alias.component.ts'), - /selector: 'pre-/)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts deleted file mode 100644 index b76308f26da3..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-basic.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ng} from '../../../utils/process'; -import {join} from 'path'; -import {expectFileToExist} from '../../../utils/fs'; - - -export default function() { - const directiveDir = join('src', 'app'); - return ng('generate', 'directive', 'test-directive') - .then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.ts'))) - .then(() => expectFileToExist(join(directiveDir, 'test-directive.directive.spec.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-in-existing-module-dir.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-in-existing-module-dir.ts deleted file mode 100644 index 7dd270fe81a1..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-in-existing-module-dir.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { join } from 'path'; -import { ng } from '../../../utils/process'; -import { expectFileToMatch } from '../../../utils/fs'; - -export default function () { - const modulePath = join('src', 'app', 'foo', 'foo.module.ts'); - - return Promise.resolve() - .then(() => ng('generate', 'module', 'foo')) - .then(() => ng('generate', 'directive', 'foo', '--no-flat')) - .then(() => expectFileToMatch(modulePath, /import { FooDirective } from '.\/foo.directive'/)); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-module-export.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-module-export.ts deleted file mode 100644 index 376b5cb762f5..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-module-export.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'directive', 'test-directive', '--export') - .then(() => expectFileToMatch(modulePath, /exports: \[\r?\n(\s*) TestDirectiveDirective\r?\n\1\]/)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-module-fail.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-module-fail.ts deleted file mode 100644 index 05ff8d98f1c9..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-module-fail.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {ng} from '../../../utils/process'; -import {expectToFail} from '../../../utils/utils'; - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => - ng('generate', 'directive', 'test-directive', '--module', 'app.moduleXXX.ts'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-module.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-module.ts deleted file mode 100644 index fb0e4f99c3db..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'directive', 'test-directive', '--module', 'app.module.ts') - .then(() => expectFileToMatch(modulePath, - /import { TestDirectiveDirective } from '.\/test-directive.directive'/)) - - .then(() => process.chdir(join('src', 'app'))) - .then(() => ng('generate', 'directive', 'test-directive2', '--module', 'app.module.ts')) - .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(modulePath, - /import { TestDirective2Directive } from '.\/test-directive2.directive'/)) - - // Try to run the unit tests. - .then(() => ng('build', '--configuration=development')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts b/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts deleted file mode 100644 index 7667915c7af3..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/directive/directive-prefix.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; -import { updateJsonFile, useCIChrome, useCIDefaults } from '../../../utils/project'; - - -export default function() { - const directiveDir = join('src', 'app'); - - return Promise.resolve() - .then(() => updateJsonFile('angular.json', configJson => { - configJson.schematics = { - '@schematics/angular:directive': { prefix: 'preW' } - }; - })) - .then(() => ng('generate', 'directive', 'test2-directive')) - .then(() => expectFileToMatch(join(directiveDir, 'test2-directive.directive.ts'), - /selector: '\[preW/)) - .then(() => ng('generate', 'application', 'app-two', '--skip-install')) - .then(() => useCIDefaults('app-two')) - .then(() => useCIChrome('./projects/app-two')) - .then(() => updateJsonFile('angular.json', configJson => { - configJson.projects['test-project'].schematics = { - '@schematics/angular:directive': { prefix: 'preP' } - }; - })) - .then(() => process.chdir('projects/app-two')) - .then(() => ng('generate', 'directive', '--skip-import', 'test3-directive')) - .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(join('projects', 'app-two', 'test3-directive.directive.ts'), - /selector: '\[preW/)) - .then(() => process.chdir('src/app')) - .then(() => ng('generate', 'directive', 'test-directive')) - .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(join(directiveDir, 'test-directive.directive.ts'), - /selector: '\[preP/)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/generate-error.ts b/tests/legacy-cli/e2e/tests/generate/generate-error.ts deleted file mode 100644 index 08d85cc02b43..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/generate-error.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {ng} from '../../utils/process'; -import {deleteFile} from '../../utils/fs'; -import {expectToFail} from '../../utils/utils'; - -export default function() { - return deleteFile('angular.json') - .then(() => expectToFail(() => ng('generate', 'class', 'hello'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/generate-name-check.ts b/tests/legacy-cli/e2e/tests/generate/generate-name-check.ts deleted file mode 100644 index a42a5cd6ca9a..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/generate-name-check.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../utils/process'; -import {expectFileToExist} from '../../utils/fs'; -import {updateJsonFile} from '../../utils/project'; - - -export default function() { - const compDir = join('src', 'app', 'test-component'); - - return Promise.resolve() - .then(() => updateJsonFile('package.json', configJson => { - delete configJson.name; - return configJson; - })) - .then(() => ng('generate', 'component', 'test-component')) - .then(() => expectFileToExist(compDir)) - .then(() => expectFileToExist(join(compDir, 'test-component.component.ts'))) - .then(() => expectFileToExist(join(compDir, 'test-component.component.spec.ts'))) - .then(() => expectFileToExist(join(compDir, 'test-component.component.html'))) - .then(() => expectFileToExist(join(compDir, 'test-component.component.css'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/generate-name-error.ts b/tests/legacy-cli/e2e/tests/generate/generate-name-error.ts deleted file mode 100644 index 2fb0583ca514..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/generate-name-error.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {ng} from '../../utils/process'; -import {expectToFail} from '../../utils/utils'; - - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => - ng('generate', 'component', '1my-component'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/guard/guard-basic.ts b/tests/legacy-cli/e2e/tests/generate/guard/guard-basic.ts deleted file mode 100644 index 7148fb0b5bf5..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/guard/guard-basic.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist, expectFileToMatch} from '../../../utils/fs'; - - -export default async function() { - // Does not create a sub directory. - const guardDir = join('src', 'app'); - - await ng('generate', 'guard', 'test-guard'); - await expectFileToExist(guardDir); - await expectFileToExist(join(guardDir, 'test-guard.guard.ts')); - await expectFileToMatch(join(guardDir, 'test-guard.guard.ts'), /implements CanActivate/); - await expectFileToExist(join(guardDir, 'test-guard.guard.spec.ts')); - await ng('test', '--watch=false'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/guard/guard-implements.ts b/tests/legacy-cli/e2e/tests/generate/guard/guard-implements.ts deleted file mode 100644 index 5dcf2a32d18c..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/guard/guard-implements.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist,expectFileToMatch} from '../../../utils/fs'; - - -export default async function() { - // Does not create a sub directory. - const guardDir = join('src', 'app'); - - await ng('generate', 'guard', 'load', '--implements=CanLoad'); - await expectFileToExist(guardDir); - await expectFileToExist(join(guardDir, 'load.guard.ts')); - await expectFileToMatch(join(guardDir, 'load.guard.ts'), /implements CanLoad/); - await expectFileToExist(join(guardDir, 'load.guard.spec.ts')); - await ng('test', '--watch=false'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/guard/guard-multiple-implements.ts b/tests/legacy-cli/e2e/tests/generate/guard/guard-multiple-implements.ts deleted file mode 100644 index a91bfd0ddde5..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/guard/guard-multiple-implements.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist,expectFileToMatch} from '../../../utils/fs'; - - -export default async function() { - // Does not create a sub directory. - const guardDir = join('src', 'app'); - - await ng('generate', 'guard', 'load', '--implements=CanLoad', '--implements=CanDeactivate'); - await expectFileToExist(guardDir); - await expectFileToExist(join(guardDir, 'load.guard.ts')); - await expectFileToMatch(join(guardDir, 'load.guard.ts'), /implements CanLoad, CanDeactivate/); - await expectFileToExist(join(guardDir, 'load.guard.spec.ts')); - await ng('test', '--watch=false'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/help-output-no-duplicates.ts b/tests/legacy-cli/e2e/tests/generate/help-output-no-duplicates.ts deleted file mode 100644 index 821333637079..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/help-output-no-duplicates.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ng } from '../../utils/process'; - -export default async function() { - // Verify that there are no duplicate options - const { stdout } = await ng('generate', 'component', '--help'); - const firstIndex = stdout.indexOf('--prefix'); - - if (firstIndex < 0) { - console.log(stdout); - throw new Error('--prefix was not part of the help output.'); - } - - if (firstIndex !== stdout.lastIndexOf('--prefix')) { - console.log(stdout); - throw new Error('--prefix first and last index were different. Possible duplicate output!'); - } -} diff --git a/tests/legacy-cli/e2e/tests/generate/help-output.ts b/tests/legacy-cli/e2e/tests/generate/help-output.ts deleted file mode 100644 index 22b0e8a397e0..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/help-output.ts +++ /dev/null @@ -1,126 +0,0 @@ -import {join} from 'path'; -import {ng, ProcessOutput} from '../../utils/process'; -import {writeMultipleFiles, createDir} from '../../utils/fs'; -import { updateJsonFile } from '../../utils/project'; - - -export default function() { - // setup temp collection - const genRoot = join('node_modules/fake-schematics/'); - - return Promise.resolve() - .then(() => createDir(genRoot)) - .then(() => writeMultipleFiles({ - [join(genRoot, 'package.json')]: ` - { - "schematics": "./collection.json" - }`, - [join(genRoot, 'collection.json')]: ` - { - "schematics": { - "fake": { - "factory": "./fake", - "description": "Fake schematic", - "schema": "./fake-schema.json" - }, - } - }`, - [join(genRoot, 'fake-schema.json')]: ` - { - "$id": "FakeSchema", - "title": "Fake Schema", - "type": "object", - "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" - } - }, - "required": [] - }`, - [join(genRoot, 'fake.js')]: ` - function def(options) { - return (host, context) => { - return host; - }; - } - exports.default = def; - `}, - )) - .then(() => ng('generate', 'fake-schematics:fake', '--help')) - .then(({stdout}) => { - if (!/ng generate fake-schematics:fake \[options\]/.test(stdout)) { - throw new Error('Help signature is wrong (1).'); - } - if (!/opt-a[\s\S]*opt-b[\s\S]*opt-c/.test(stdout)) { - throw new Error('Help signature options are incorrect.'); - } - }) - // set up default collection. - .then(() => updateJsonFile('angular.json', json => { - json.cli = json.cli || {} as any; - json.cli.defaultCollection = 'fake-schematics'; - })) - .then(() => ng('generate', 'fake', '--help')) - // verify same output - .then(({stdout}) => { - if (!/ng generate fake \[options\]/.test(stdout)) { - throw new Error('Help signature is wrong (2).'); - } - if (!/opt-a[\s\S]*opt-b[\s\S]*opt-c/.test(stdout)) { - throw new Error('Help signature options are incorrect.'); - } - }) - - // should print all the available schematics in a collection - // when a collection has more than 1 schematic - .then(() => writeMultipleFiles({ - [join(genRoot, 'collection.json')]: ` - { - "schematics": { - "fake": { - "factory": "./fake", - "description": "Fake schematic", - "schema": "./fake-schema.json" - }, - "fake-two": { - "factory": "./fake", - "description": "Fake schematic", - "schema": "./fake-schema.json" - }, - } - }`, - })) - .then(() => ng('generate', '--help')) - .then(({stdout}) => { - if (!/Collection \"fake-schematics\" \(default\):[\s\S]*fake[\s\S]*fake-two/.test(stdout)) { - throw new Error( - `Help result is wrong, it didn't contain all the schematics.`); - } - }); - -} diff --git a/tests/legacy-cli/e2e/tests/generate/interceptor/interceptor-basic.ts b/tests/legacy-cli/e2e/tests/generate/interceptor/interceptor-basic.ts deleted file mode 100755 index 19e27eeed5dc..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/interceptor/interceptor-basic.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; - - -export default function() { - // Does not create a sub directory. - const interceptorDir = join('src', 'app'); - - return ng('generate', 'interceptor', 'test-interceptor') - .then(() => expectFileToExist(interceptorDir)) - .then(() => expectFileToExist(join(interceptorDir, 'test-interceptor.interceptor.ts'))) - .then(() => expectFileToExist(join(interceptorDir, 'test-interceptor.interceptor.spec.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/interface.ts b/tests/legacy-cli/e2e/tests/generate/interface.ts deleted file mode 100644 index e74f2570f4f1..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../utils/process'; -import {expectFileToExist} from '../../utils/fs'; - - -export default function() { - const interfaceDir = join('src', 'app'); - - return ng('generate', 'interface', 'test-interface', 'model') - .then(() => expectFileToExist(interfaceDir)) - .then(() => expectFileToExist(join(interfaceDir, 'test-interface.model.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts deleted file mode 100644 index b523bbc67bb1..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { writeFile } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -export default async function () { - await ng('generate', 'library', 'my-lib'); - - await writeFile('./src/app/app.module.ts', ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { MyLibModule } from 'my-lib'; - - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - MyLibModule, - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `); - - await writeFile('./src/app/app.component.ts', ` - import { Component } from '@angular/core'; - import { MyLibService } from 'my-lib'; - - @Component({ - selector: 'app-root', - template: '' - }) - export class AppComponent { - title = 'test-project'; - - constructor(myLibService: MyLibService) { - console.log(myLibService); - } - } - `); - - await writeFile('e2e/src/app.e2e-spec.ts', ` - import { browser, logging, element, by } from 'protractor'; - import { AppPage } from './app.po'; - - describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display text from library component', async () => { - await page.navigateTo(); - expect(await element(by.css('lib-my-lib p')).getText()).toEqual('my-lib works!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - })); - }); - }); - `); - - // Build library in full mode (development) - await ng('build', 'my-lib', '--configuration=development'); - - // AOT linking - await runTests(); - - // JIT linking - await updateJsonFile('angular.json', config => { - const build = config.projects['test-project'].architect.build; - build.options.aot = false; - build.configurations.production.buildOptimizer = false; - }); - - await runTests(); -} - -async function runTests(): Promise { - // Check that the tests succeeds both with named project, unnamed (should test app), and prod. - await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts deleted file mode 100644 index 076ceb65cd80..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { writeFile } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -export default async function () { - await ng('generate', 'library', 'my-lib'); - - await writeFile('./src/app/app.module.ts', ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { MyLibModule } from 'my-lib'; - - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - MyLibModule, - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `); - - await writeFile('./src/app/app.component.ts', ` - import { Component } from '@angular/core'; - import { MyLibService } from 'my-lib'; - - @Component({ - selector: 'app-root', - template: '' - }) - export class AppComponent { - title = 'test-project'; - - constructor(myLibService: MyLibService) { - console.log(myLibService); - } - } - `); - - await writeFile('e2e/src/app.e2e-spec.ts', ` - import { browser, logging, element, by } from 'protractor'; - import { AppPage } from './app.po'; - - describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display text from library component', async () => { - await page.navigateTo(); - expect(await element(by.css('lib-my-lib p')).getText()).toEqual('my-lib works!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - })); - }); - }); - `); - - // Build library in partial mode (production) - await ng('build', 'my-lib', '--configuration=production'); - - // AOT linking - await runTests(); - - // JIT linking - await updateJsonFile('angular.json', config => { - const build = config.projects['test-project'].architect.build; - build.options.aot = false; - build.configurations.production.buildOptimizer = false; - }); - - await runTests(); -} - -async function runTests(): Promise { - // Check that the tests succeeds both with named project, unnamed (should test app), and prod. - await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts deleted file mode 100644 index 34c4114fdf8f..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { writeFile } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; -import { updateJsonFile } from '../../../utils/project'; - -export default async function () { - await ng('generate', 'library', 'my-lib'); - - await updateJsonFile('projects/my-lib/tsconfig.lib.prod.json', config => { - const { angularCompilerOptions = {} } = config; - angularCompilerOptions.enableIvy = false; - angularCompilerOptions.skipTemplateCodegen = true; - angularCompilerOptions.strictMetadataEmit = true; - config.angularCompilerOptions = angularCompilerOptions; - }); - - await writeFile('./src/app/app.module.ts', ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { MyLibModule } from 'my-lib'; - - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - MyLibModule, - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `); - - await writeFile('./src/app/app.component.ts', ` - import { Component } from '@angular/core'; - import { MyLibService } from 'my-lib'; - - @Component({ - selector: 'app-root', - template: '' - }) - export class AppComponent { - title = 'test-project'; - - constructor(myLibService: MyLibService) { - console.log(myLibService); - } - } - `); - - await writeFile('e2e/src/app.e2e-spec.ts', ` - import { browser, logging, element, by } from 'protractor'; - import { AppPage } from './app.po'; - - describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display text from library component', async () => { - await page.navigateTo(); - expect(await element(by.css('lib-my-lib p')).getText()).toEqual('my-lib works!'); - }); - - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - })); - }); - }); - `); - - // Build library in VE mode (production) - await ng('build', 'my-lib', '--configuration=production'); - - // AOT linking - await runTests(); - - // JIT linking - await updateJsonFile('angular.json', config => { - const build = config.projects['test-project'].architect.build; - build.options.aot = false; - build.configurations.production.buildOptimizer = false; - }); - - await runTests(); -} - -async function runTests(): Promise { - // Check that the tests succeeds both with named project, unnamed (should test app), and prod. - await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/module/module-basic.ts b/tests/legacy-cli/e2e/tests/generate/module/module-basic.ts deleted file mode 100644 index b78c3c22a112..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/module/module-basic.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist, expectFileToMatch} from '../../../utils/fs'; -import {expectToFail} from '../../../utils/utils'; - - -export default function() { - const moduleDir = join('src', 'app', 'test'); - - return ng('generate', 'module', 'test') - .then(() => expectFileToExist(moduleDir)) - .then(() => expectFileToExist(join(moduleDir, 'test.module.ts'))) - .then(() => expectToFail(() => expectFileToExist(join(moduleDir, 'test-routing.module.ts')))) - .then(() => expectToFail(() => expectFileToExist(join(moduleDir, 'test.spec.ts')))) - .then(() => expectFileToMatch(join(moduleDir, 'test.module.ts'), 'TestModule')) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/module/module-import.ts b/tests/legacy-cli/e2e/tests/generate/module/module-import.ts deleted file mode 100644 index 1e00993bc858..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/module/module-import.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { join } from 'path'; -import { ng } from '../../../utils/process'; -import { expectFileToMatch } from '../../../utils/fs'; - -export default function () { - const root = process.cwd(); - const modulePath = join(root, 'src', 'app', 'app.module.ts'); - const subModulePath = join('src', 'app', 'sub', 'sub.module.ts'); - const deepSubModulePath = join('src', 'app', 'sub', 'deep', 'deep.module.ts'); - - return Promise.resolve() - .then(() => ng('generate', 'module', 'sub')) - .then(() => ng('generate', 'module', 'sub/deep')) - - .then(() => ng('generate', 'module', 'test1', '--module', 'app.module.ts')) - .then(() => expectFileToMatch(modulePath, - /import { Test1Module } from '.\/test1\/test1.module'/)) - .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test1Module(.|\s)*\]/m)) - - .then(() => ng('generate', 'module', 'test2', '--module', 'app.module')) - .then(() => expectFileToMatch(modulePath, - /import { Test2Module } from '.\/test2\/test2.module'/)) - .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test2Module(.|\s)*\]/m)) - - .then(() => ng('generate', 'module', 'test3', '--module', 'app')) - .then(() => expectFileToMatch(modulePath, - /import { Test3Module } from '.\/test3\/test3.module'/)) - .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test3Module(.|\s)*\]/m)) - - .then(() => ng('generate', 'module', 'test4', '--routing', '--module', 'app')) - .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test4Module(.|\s)*\]/m)) - .then(() => expectFileToMatch(join('src', 'app', 'test4', 'test4.module.ts'), - /import { Test4RoutingModule } from '.\/test4-routing.module'/)) - .then(() => expectFileToMatch(join('src', 'app', 'test4', 'test4.module.ts'), - /imports: \[(.|\s)*Test4RoutingModule(.|\s)*\]/m)) - - .then(() => ng('generate', 'module', 'test5', '--module', 'sub')) - .then(() => expectFileToMatch(subModulePath, - /import { Test5Module } from '..\/test5\/test5.module'/)) - .then(() => expectFileToMatch(subModulePath, /imports: \[(.|\s)*Test5Module(.|\s)*\]/m)) - - .then(() => ng('generate', 'module', 'test6', '--module', join('sub', 'deep')) - .then(() => expectFileToMatch(deepSubModulePath, - /import { Test6Module } from '..\/..\/test6\/test6.module'/)) - .then(() => expectFileToMatch(deepSubModulePath, /imports: \[(.|\s)*Test6Module(.|\s)*\]/m))); - - // E2E_DISABLE: temporarily disable pending investigation - // .then(() => process.chdir(join(root, 'src', 'app'))) - // .then(() => ng('generate', 'module', 'test7', '--module', 'app.module.ts')) - // .then(() => process.chdir('..')) - // .then(() => expectFileToMatch(modulePath, - // /import { Test7Module } from '.\/test7\/test7.module'/)) - // .then(() => expectFileToMatch(modulePath, /imports: \[(.|\s)*Test7Module(.|\s)*\]/m)); -} diff --git a/tests/legacy-cli/e2e/tests/generate/module/module-route.ts b/tests/legacy-cli/e2e/tests/generate/module/module-route.ts deleted file mode 100644 index 5dafaa581660..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/module/module-route.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expectFileToExist } from '../../../utils/fs'; -import { ng } from '../../../utils/process'; - -export default async function () { - await ng('config', 'schematics.@schematics/angular.component.style', 'scss'); - - await ng('generate', 'module', 'test', '--routing'); - - await ng('generate', 'module', 'home', '-m', 'test', '--route', 'home', '--routing'); - - await expectFileToExist('src/app/home/home.component.scss'); -} diff --git a/tests/legacy-cli/e2e/tests/generate/module/module-routing-child-folder.ts b/tests/legacy-cli/e2e/tests/generate/module/module-routing-child-folder.ts deleted file mode 100644 index 240588a8967d..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/module/module-routing-child-folder.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { join } from 'path'; -import { ng } from '../../../utils/process'; -import { expectFileToExist } from '../../../utils/fs'; -import { expectToFail } from '../../../utils/utils'; - - -export default function () { - const root = process.cwd(); - const testPath = join(root, 'src', 'app'); - - process.chdir(testPath); - - return Promise.resolve() - .then(() => - ng('generate', 'module', 'sub-dir/child', '--routing') - .then(() => expectFileToExist(join(testPath, 'sub-dir/child'))) - .then(() => expectFileToExist(join(testPath, 'sub-dir/child', 'child.module.ts'))) - .then(() => expectFileToExist(join(testPath, 'sub-dir/child', 'child-routing.module.ts'))) - .then(() => expectToFail(() => - expectFileToExist(join(testPath, 'sub-dir/child', 'child.spec.ts')) - )) - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')) - ); -} diff --git a/tests/legacy-cli/e2e/tests/generate/module/module-routing.ts b/tests/legacy-cli/e2e/tests/generate/module/module-routing.ts deleted file mode 100644 index cf6208b0a84c..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/module/module-routing.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; -import {expectToFail} from '../../../utils/utils'; - - -export default function() { - const moduleDir = join('src', 'app', 'test'); - - return ng('generate', 'module', 'test', '--routing') - .then(() => expectFileToExist(moduleDir)) - .then(() => expectFileToExist(join(moduleDir, 'test.module.ts'))) - .then(() => expectFileToExist(join(moduleDir, 'test-routing.module.ts'))) - .then(() => expectToFail(() => expectFileToExist(join(moduleDir, 'test.spec.ts')))) - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-basic.ts b/tests/legacy-cli/e2e/tests/generate/pipe/pipe-basic.ts deleted file mode 100644 index 0480c13cd2ff..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-basic.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; - -import {expectFileToExist} from '../../../utils/fs'; - -export default function() { - // Create the pipe in the same directory. - const pipeDir = join('src', 'app'); - - return ng('generate', 'pipe', 'test-pipe') - .then(() => expectFileToExist(pipeDir)) - .then(() => expectFileToExist(join(pipeDir, 'test-pipe.pipe.ts'))) - .then(() => expectFileToExist(join(pipeDir, 'test-pipe.pipe.spec.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-in-existing-module-dir.ts b/tests/legacy-cli/e2e/tests/generate/pipe/pipe-in-existing-module-dir.ts deleted file mode 100644 index 2055643a1b5b..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-in-existing-module-dir.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { join } from 'path'; -import { ng } from '../../../utils/process'; -import { expectFileToMatch } from '../../../utils/fs'; - -export default function () { - const modulePath = join('src', 'app', 'foo', 'foo.module.ts'); - - return Promise.resolve() - .then(() => ng('generate', 'module', 'foo')) - .then(() => ng('generate', 'pipe', 'foo', '--no-flat')) - .then(() => expectFileToMatch(modulePath, /import { FooPipe } from '.\/foo.pipe'/)); -} diff --git a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-export.ts b/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-export.ts deleted file mode 100644 index 4773b2b5e231..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-export.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'pipe', 'test-pipe', '--export') - .then(() => expectFileToMatch(modulePath, /exports: \[\r?\n(\s*) TestPipePipe\r?\n\1\]/)) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-fail.ts b/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-fail.ts deleted file mode 100644 index e18d5e73d038..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module-fail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {ng} from '../../../utils/process'; -import {expectToFail} from '../../../utils/utils'; - - -export default function() { - return Promise.resolve() - .then(() => expectToFail(() => - ng('generate', 'pipe', 'test-pipe', '--module', 'app.moduleXXX.ts'))); -} diff --git a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module.ts b/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module.ts deleted file mode 100644 index 274e0079d16f..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/pipe/pipe-module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToMatch} from '../../../utils/fs'; - - -export default function() { - const modulePath = join('src', 'app', 'app.module.ts'); - - return ng('generate', 'pipe', 'test-pipe', '--module', 'app.module.ts') - .then(() => expectFileToMatch(modulePath, - /import { TestPipePipe } from '.\/test-pipe.pipe'/)) - - .then(() => process.chdir(join('src', 'app'))) - .then(() => ng('generate', 'pipe', 'test-pipe2', '--module', 'app.module.ts')) - .then(() => process.chdir('../..')) - .then(() => expectFileToMatch(modulePath, - /import { TestPipe2Pipe } from '.\/test-pipe2.pipe'/)) - - // Try to run the unit tests. - .then(() => ng('build', '--configuration=development')); -} diff --git a/tests/legacy-cli/e2e/tests/generate/service/service-basic.ts b/tests/legacy-cli/e2e/tests/generate/service/service-basic.ts deleted file mode 100644 index 3ebd58fc977c..000000000000 --- a/tests/legacy-cli/e2e/tests/generate/service/service-basic.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {join} from 'path'; -import {ng} from '../../../utils/process'; -import {expectFileToExist} from '../../../utils/fs'; - - -export default function() { - // Does not create a sub directory. - const serviceDir = join('src', 'app'); - - return ng('generate', 'service', 'test-service') - .then(() => expectFileToExist(serviceDir)) - .then(() => expectFileToExist(join(serviceDir, 'test-service.service.ts'))) - .then(() => expectFileToExist(join(serviceDir, 'test-service.service.spec.ts'))) - - // Try to run the unit tests. - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/extract-ivy-libraries.ts b/tests/legacy-cli/e2e/tests/i18n/extract-ivy-libraries.ts deleted file mode 100644 index 442f64ea2d71..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/extract-ivy-libraries.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; -import { installPackage, uninstallPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { readNgVersion } from '../../utils/version'; - -export default async function() { - // Setup a library - await ng('generate', 'library', 'i18n-lib-test'); - await replaceInFile( - 'projects/i18n-lib-test/src/lib/i18n-lib-test.component.ts', - '

', - '

', - ); - - // Build library - await ng('build', 'i18n-lib-test', '--configuration=development'); - - // Consume library in application - await writeFile( - 'src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { I18nLibTestModule } from 'i18n-lib-test'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - I18nLibTestModule, - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, - ); - - await writeFile( - 'src/app/app.component.html', - ` -

Hello world

- - `, - ); - - // Install correct version - let localizeVersion = '@angular/localize@' + readNgVersion(); - if (getGlobalVariable('argv')['ng-snapshots']) { - localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; - } - await installPackage(localizeVersion); - - // Extract messages - await ng('extract-i18n'); - await expectFileToMatch('messages.xlf', 'Hello world'); - await expectFileToMatch('messages.xlf', 'i18n-lib-test works!'); - await expectFileToMatch('messages.xlf', 'src/app/app.component.html'); - await expectFileToMatch( - 'messages.xlf', - 'projects/i18n-lib-test/src/lib/i18n-lib-test.component.ts', - ); - - await uninstallPackage('@angular/localize'); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/extract-ivy.ts b/tests/legacy-cli/e2e/tests/i18n/extract-ivy.ts deleted file mode 100644 index a64eb2604e0f..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/extract-ivy.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { join } from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { writeFile } from '../../utils/fs'; -import { installPackage, uninstallPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { readNgVersion } from '../../utils/version'; - -export default async function() { - // Setup an i18n enabled component - await ng('generate', 'component', 'i18n-test'); - await writeFile( - join('src/app/i18n-test', 'i18n-test.component.html'), - '

Hello world

', - ); - - // Should fail if `@angular/localize` is missing - const { message: message1 } = await expectToFail(() => ng('extract-i18n')); - if (!message1.includes(`Ivy extraction requires the '@angular/localize' package.`)) { - throw new Error('Expected localize package error message when missing'); - } - - // Install correct version - let localizeVersion = '@angular/localize@' + readNgVersion(); - if (getGlobalVariable('argv')['ng-snapshots']) { - localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; - } - await installPackage(localizeVersion); - - // Should not show any warnings when extracting - const { stderr: message5 } = await ng('extract-i18n'); - if (message5.includes('WARNING')) { - throw new Error('Expected no warnings to be shown'); - } - - // Disable Ivy - await updateJsonFile('tsconfig.json', config => { - const { angularCompilerOptions = {} } = config; - angularCompilerOptions.enableIvy = false; - config.angularCompilerOptions = angularCompilerOptions; - }); - - // Should show ivy disabled application warning with enableIvy false - const { stderr: message4 } = await ng('extract-i18n'); - if (!message4.includes(`Ivy extraction enabled but application is not Ivy enabled.`)) { - throw new Error('Expected ivy disabled application warning'); - } - - await uninstallPackage('@angular/localize'); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts deleted file mode 100644 index 5f72c6dd7470..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { - appendToFile, - copyFile, - expectFileToExist, - expectFileToMatch, - replaceInFile, - writeFile, -} from '../../utils/fs'; -import { installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { readNgVersion } from '../../utils/version'; - -const snapshots = require('../../ng-snapshot/package.json'); - -export default async function () { - // TEMP: disable pending i18n updates - // TODO: when re-enabling, use setupI18nConfig and helpers like other i18n tests. - return; - - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - dependencies['@angular/localize'] = isSnapshotBuild - ? snapshots.dependencies['@angular/localize'] - : readNgVersion(); - }); - - await appendToFile('src/app/app.component.html', ''); - await ng('generate', 'appShell', '--project', 'test-project'); - - if (isSnapshotBuild) { - await updateJsonFile('package.json', (packageJson) => { - const dependencies = packageJson['dependencies']; - dependencies['@angular/platform-server'] = snapshots.dependencies['@angular/platform-server']; - dependencies['@angular/router'] = snapshots.dependencies['@angular/router']; - }); - } - - await installWorkspacePackages(); - - const browserBaseDir = 'dist/test-project/browser'; - - // Set configurations for each locale. - const langTranslations = [ - { lang: 'en-US', translation: 'Hello i18n!' }, - { lang: 'fr', translation: 'Bonjour i18n!' }, - ]; - - await updateJsonFile('angular.json', (workspaceJson) => { - const appProject = workspaceJson.projects['test-project']; - const appArchitect = appProject.architect || appProject.targets; - const buildOptions = appArchitect['build'].options; - const serverOptions = appArchitect['server'].options; - - // Make default builds prod. - buildOptions.optimization = true; - buildOptions.buildOptimizer = true; - buildOptions.aot = true; - buildOptions.fileReplacements = [ - { - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts', - }, - ]; - - serverOptions.optimization = true; - serverOptions.fileReplacements = [ - { - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts', - }, - ]; - - // Enable localization for all locales - buildOptions.localize = true; - serverOptions.localize = true; - - // Add locale definitions to the project - // tslint:disable-next-line: no-any - const i18n: Record = (appProject.i18n = { locales: {} }); - for (const { lang } of langTranslations) { - if (lang == 'en-US') { - i18n.sourceLocale = lang; - } else { - i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; - } - } - }); - - await writeFile( - 'src/app/app-shell/app-shell.component.html', - '

Hello i18n!

', - ); - - // Add a translatable element - // Extraction of i18n only works on browser targets. - // Let's add the same translation that there is in the app-shell - await writeFile( - 'src/app/app.component.html', - '

Hello i18n!

', - ); - - // Extract the translation messages and copy them for each language. - await ng('extract-i18n', '--output-path=src/locale'); - await expectFileToExist('src/locale/messages.xlf'); - await expectFileToMatch('src/locale/messages.xlf', `source-language="en-US"`); - await expectFileToMatch('src/locale/messages.xlf', `An introduction header for this sample`); - - // Clean up app.component.html so that we can easily - // find the translation text - await writeFile('src/app/app.component.html', ''); - - for (const { lang, translation } of langTranslations) { - if (lang != 'en-US') { - await copyFile('src/locale/messages.xlf', `src/locale/messages.${lang}.xlf`); - await replaceInFile( - `src/locale/messages.${lang}.xlf`, - 'source-language="en-US"', - `source-language="en-US" target-language="${lang}"`, - ); - await replaceInFile( - `src/locale/messages.${lang}.xlf`, - 'Hello i18n!', - `Hello i18n!\n${translation}`, - ); - } - } - - // Build each locale and verify the output. - await ng('run', 'test-project:app-shell'); - - for (const { lang, translation } of langTranslations) { - await expectFileToMatch(`${browserBaseDir}/${lang}/index.html`, translation); - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts deleted file mode 100644 index 4aa4d5a3cdbb..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { externalServer, langTranslations, setupI18nConfig } from './setup'; - -const baseHrefs = { - 'en-US': '/en/', - fr: '/fr-FR/', - de: '', -}; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.sourceLocale = { - baseHref: baseHrefs['en-US'], - }; - - i18n.locales['fr'] = { - translation: i18n.locales['fr'], - baseHref: baseHrefs['fr'], - }; - - i18n.locales['de'] = { - translation: i18n.locales['de'], - baseHref: baseHrefs['de'], - }; - }); - - // Build each locale and verify the output. - await ng('build'); - for (const { lang, outputPath } of langTranslations) { - if (baseHrefs[lang] === undefined) { - throw new Error('Invalid E2E test setup: unexpected locale ' + lang); - } - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - - // Verify the HTML base HREF attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `href="${baseHrefs[lang] || '/'}"`); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, baseHrefs[lang] || '/'); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200${baseHrefs[lang] || '/'}`, - ); - } finally { - server.close(); - } - } - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - - appArchitect['build'].options.baseHref = '/test/'; - }); - - // Build each locale and verify the output. - await ng('build', '--configuration=development'); - for (const { lang, outputPath } of langTranslations) { - // Verify the HTML base HREF attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `href="/test${baseHrefs[lang] || '/'}"`); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, '/test' + (baseHrefs[lang] || '/')); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/test${baseHrefs[lang] || '/'}`, - ); - } finally { - server.close(); - } - } - - // Test absolute base href. - await ng('build', '--base-href', 'http://www.domain.com/', '--configuration=development'); - for (const { lang, outputPath } of langTranslations) { - // Verify the HTML base HREF attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `href="http://www.domain.com${baseHrefs[lang] || '/'}"`); - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-arb.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-arb.ts deleted file mode 100644 index 8461b5c1eec4..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-arb.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { executeTest } from './ivy-localize-dl-xliff2'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig('arb'); - - // Execute the tests - await executeTest(); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-json.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-json.ts deleted file mode 100644 index 955ae0c70af7..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-json.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { executeTest } from './ivy-localize-dl-xliff2'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig('json'); - - // Execute the tests - await executeTest(); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff1.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff1.ts deleted file mode 100644 index 33beb1cdd29b..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff1.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { executeTest } from './ivy-localize-dl-xliff2'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig('xlf'); - - // Execute the tests - await executeTest(); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts deleted file mode 100644 index 87159d36a5f0..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xliff2.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { appendToFile, expectFileToMatch, replaceInFile } from '../../utils/fs'; -import { execAndWaitForOutputToMatch, killAllProcesses, ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { baseDir, externalServer, langTranslations, setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig('xlf2'); - - // Execute the tests - await executeTest(); -} - -export async function executeTest() { - // Ensure a DL build is used. - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - await updateJsonFile('tsconfig.json', config => { - config.compilerOptions.target = 'es2017'; - if (!config.angularCompilerOptions) { - config.angularCompilerOptions = {}; - } - config.angularCompilerOptions.disableTypeScriptVersionCheck = true; - }); - - // Build each locale and verify the output. - await ng('build'); - for (const { lang, outputPath, translation } of langTranslations) { - await expectFileToMatch(`${outputPath}/main-es5.js`, translation.helloPartial); - await expectFileToMatch(`${outputPath}/main-es2017.js`, translation.helloPartial); - await expectToFail(() => expectFileToMatch(`${outputPath}/main-es5.js`, '$localize`')); - await expectToFail(() => expectFileToMatch(`${outputPath}/main-es2017.js`, '$localize`')); - - // Verify the locale ID is present - await expectFileToMatch(`${outputPath}/vendor-es5.js`, lang); - await expectFileToMatch(`${outputPath}/vendor-es2017.js`, lang); - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - - // Verify the HTML base HREF attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `href="/${lang}/"`); - - // Verify the locale data is registered using the global files - await expectFileToMatch(`${outputPath}/vendor-es5.js`, '.ng.common.locales'); - await expectFileToMatch(`${outputPath}/vendor-es2017.js`, '.ng.common.locales'); - - // Verify the locale data is browser compatible - await expectToFail(() => expectFileToMatch(`${outputPath}/vendor-es5.js`, /\bconst\b/)); - await expectFileToMatch(`${outputPath}/vendor-es2017.js`, /\bconst\b/); - - // Verify locale data comments are removed in production - await expectToFail(() => - expectFileToMatch(`${outputPath}/vendor-es5.js`, '// See angular/tools/gulp-tasks/cldr/extract.js'), - ); - await expectToFail(() => - expectFileToMatch(`${outputPath}/vendor-es2017.js`, '// See angular/tools/gulp-tasks/cldr/extract.js'), - ); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, - ); - } finally { - server.close(); - } - } - - // Verify deprecated locale data registration is not present - await ng('build', '--configuration=fr', '--optimization=false', '--configuration=development'); - await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es5.js`, 'registerLocaleData(')); - await expectToFail(() => - expectFileToMatch(`${baseDir}/fr/main-es2017.js`, 'registerLocaleData('), - ); - - // Verify missing translation behaviour. - await appendToFile('src/app/app.component.html', '

Other content

'); - await ng('build', '--i18n-missing-translation', 'ignore', '--configuration=development'); - await expectFileToMatch(`${baseDir}/fr/main-es5.js`, /Other content/); - await expectFileToMatch(`${baseDir}/fr/main-es2017.js`, /Other content/); - await expectToFail(() => ng('build', '--configuration=development')); - try { - await execAndWaitForOutputToMatch( - 'ng', - ['serve', '--configuration=fr', '--port=0'], - /No translation found for/, - ); - } finally { - killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xmb.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xmb.ts deleted file mode 100644 index cfc116d43870..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl-xmb.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { executeTest } from './ivy-localize-dl-xliff2'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig('xmb'); - - // Execute the tests - await executeTest(); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts deleted file mode 100644 index bbad96799cfb..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expectFileNotToExist, expectFileToMatch, readFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { externalServer, langTranslations, setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Ensure a es2017 build is used. - await writeFile('.browserslistrc', 'Chrome 65'); - await updateJsonFile('tsconfig.json', config => { - config.compilerOptions.target = 'es2017'; - if (!config.angularCompilerOptions) { - config.angularCompilerOptions = {}; - } - config.angularCompilerOptions.disableTypeScriptVersionCheck = true; - }); - - await ng('build', '--source-map'); - for (const { lang, outputPath, translation } of langTranslations) { - await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial); - await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`')); - await expectFileNotToExist(`${outputPath}/main-es5.js`); - - // Ensure sourcemap for modified file contains content - const mainSourceMap = JSON.parse(await readFile(`${outputPath}/main.js.map`)); - if ( - mainSourceMap.version !== 3 || - !Array.isArray(mainSourceMap.sources) || - typeof mainSourceMap.mappings !== 'string' - ) { - throw new Error('invalid localized sourcemap for main.js'); - } - - // Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references) - // The only reference in a new application is in @angular/core - await expectFileToMatch(`${outputPath}/vendor.js`, lang); - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, - ); - } finally { - server.close(); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts deleted file mode 100644 index 207e0c6a5e46..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expectFileNotToExist, expectFileToMatch, readFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { externalServer, langTranslations, setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Ensure a es5 build is used. - await updateJsonFile('tsconfig.json', config => { - config.compilerOptions.target = 'es5'; - if (!config.angularCompilerOptions) { - config.angularCompilerOptions = {}; - } - config.angularCompilerOptions.disableTypeScriptVersionCheck = true; - }); - - // Build each locale and verify the output. - await ng('build'); - for (const { lang, outputPath, translation } of langTranslations) { - await expectFileToMatch(`${outputPath}/main.js`, translation.helloPartial); - await expectToFail(() => expectFileToMatch(`${outputPath}/main.js`, '$localize`')); - await expectFileNotToExist(`${outputPath}/main-es2017.js`); - - // Ensure sourcemap for modified file contains content - const mainSourceMap = JSON.parse(await readFile(`${outputPath}/main.js.map`)); - if ( - mainSourceMap.version !== 3 || - !Array.isArray(mainSourceMap.sources) || - typeof mainSourceMap.mappings !== 'string' - ) { - throw new Error('invalid localized sourcemap for main.js'); - } - - // Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references) - // The only reference in a new application is in @angular/core - await expectFileToMatch(`${outputPath}/vendor.js`, lang); - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, - ); - } finally { - server.close(); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts deleted file mode 100644 index 3c0d8b59a39f..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expectFileToMatch, prependToFile, readFile, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { externalServer, langTranslations, setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Update angular.json to only localize one locale - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - appProject.architect['build'].options.localize = ['fr']; - }); - - // Update E2E test to look for augmented locale data - await replaceInFile('./e2e/src/app.fr.e2e-spec.ts', 'janvier', 'changed-janvier'); - - // Augment the locale data and import into the main application file - const localeData = await readFile('node_modules/@angular/common/locales/global/fr.js'); - await writeFile('src/fr-changed.js', localeData.replace('janvier', 'changed-janvier')); - await prependToFile('src/main.ts', 'import \'./fr-changed.js\';\n'); - - // Run a build and test - await ng('build'); - for (const { lang, outputPath } of langTranslations) { - // Only the fr locale was built for this test - if (lang !== 'fr') { - continue; - } - - // Ensure locale is inlined (@angular/localize plugin inlines `$localize.locale` references) - // The only reference in a new application is in @angular/core - await expectFileToMatch(`${outputPath}/vendor.js`, lang); - - // Execute Application E2E tests with dev server - await ng('e2e', `--configuration=${lang}`, '--port=0'); - - // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); - try { - await ng( - 'e2e', - `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, - ); - } finally { - server.close(); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data.ts deleted file mode 100644 index 4356a93103f6..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.sourceLocale = 'fr-Abcd'; - appProject.architect['build'].options.localize = ['fr-Abcd']; - }); - - const { stderr: err1 } = await ng('build'); - if (!err1.includes(`Locale data for 'fr-Abcd' cannot be found. Using locale data for 'fr'.`)) { - throw new Error('locale data fallback warning not shown'); - } - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.sourceLocale = 'en-US'; - appProject.architect['build'].options.localize = ['en-US']; - }); - - const { stderr: err2 } = await ng('build'); - if (err2.includes(`Locale data for 'en-US' cannot be found. No locale data will be included for this locale.`)) { - throw new Error('locale data not found warning shown'); - } - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.sourceLocale = 'en-x-abc'; - appProject.architect['build'].options.localize = ['en-x-abc']; - }); - - const { stderr: err3 } = await ng('build', '--configuration=development'); - if (err3.includes(`Locale data for 'en-x-abc' cannot be found. No locale data will be included for this locale.`)) { - throw new Error('locale data not found warning shown'); - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-merging.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-merging.ts deleted file mode 100644 index 622d12d5bff8..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-merging.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.locales['fr'] = [ - i18n.locales['fr'], - i18n.locales['fr'], - ] - appProject.architect['build'].options.localize = ['fr']; - }); - - const { stderr: err1 } = await ng('build'); - if (!err1.includes('Duplicate translations for message')) { - throw new Error('duplicate translations warning not shown'); - } - - -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts deleted file mode 100644 index c47c96fb8ed8..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts +++ /dev/null @@ -1,128 +0,0 @@ -import * as express from 'express'; -import { join } from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { appendToFile, expectFileToMatch, writeFile } from '../../utils/fs'; -import { installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { langTranslations, setupI18nConfig } from './setup'; - -const snapshots = require('../../ng-snapshot/package.json'); - -export default async function () { - // TODO: Re-enable pending further Ivy/Universal/i18n work - return; - - // Setup i18n tests and config. - await setupI18nConfig(); - - // Add universal to the project - const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; - await ng('add', '@nguniversal/express-engine@9.0.0-next.6', '--skip-install'); - - if (isSnapshotBuild) { - await updateJsonFile('package.json', packageJson => { - const dependencies = packageJson['dependencies']; - dependencies['@angular/platform-server'] = snapshots.dependencies['@angular/platform-server']; - }); - } - - await installWorkspacePackages(); - - const serverbaseDir = 'dist/test-project/server'; - const serverBuildArgs = ['run', 'test-project:server']; - - // Add server-specific config. - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - const appArchitect = appProject.architect || appProject.targets; - const serverOptions = appArchitect['server'].options; - - serverOptions.optimization = true; - serverOptions.fileReplacements = [ - { - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts', - }, - ]; - - // Enable localization for all locales - // TODO: re-enable all locales once localeData support is added. - // serverOptions.localize = true; - serverOptions.localize = ['fr']; - // Always error on missing translations. - serverOptions.i18nMissingTranslation = 'error'; - }); - - // Override 'main.ts' so that we never bootstrap the client side - // This is needed so that we can we can run E2E test against the server view - await writeFile( - 'src/main.ts', - ` - import { enableProdMode } from '@angular/core'; - - import { AppModule } from './app/app.module'; - import { environment } from './environments/environment'; - - if (environment.production) { - enableProdMode(); - } - `, - ); - - // By default the 'server.ts' doesn't support localized dist folders, - // so we create a copy of 'app' function with a locale parameter. - await appendToFile( - 'server.ts', - ` - export function i18nApp(locale: string) { - const server = express(); - const distFolder = join(process.cwd(), \`dist/test-project/browser/\${locale}\`); - - server.engine('html', ngExpressEngine({ - bootstrap: AppServerModule, - })); - - server.set('view engine', 'html'); - server.set('views', distFolder); - - server.get('*.*', express.static(distFolder, { - maxAge: '1y' - })); - - server.get('*', (req, res) => { - res.render('index', { req }); - }); - - return server; - } - `, - ); - - // Build each locale and verify the output. - await ng('build'); - await ng(...serverBuildArgs); - - for (const { lang, translation } of langTranslations) { - await expectFileToMatch(`${serverbaseDir}/${lang}/main.js`, translation.helloPartial); - await expectToFail(() => expectFileToMatch(`${serverbaseDir}/${lang}/main.js`, '$localize`')); - - // Run the server - const serverBundle = join(process.cwd(), `${serverbaseDir}/${lang}/main.js`); - const { i18nApp } = await import(serverBundle) as { i18nApp(locale: string): express.Express }; - const server = i18nApp(lang).listen(4200, 'localhost'); - try { - // Execute without a devserver. - await ng('e2e', `--configuration=${lang}`, '--devServerTarget='); - } finally { - server.close(); - } - } - - // Verify missing translation behaviour. - await appendToFile('src/app/app.component.html', '

Other content

'); - await ng(...serverBuildArgs, '--i18n-missing-translation', 'ignore'); - await expectFileToMatch(`${serverbaseDir}/fr/main.js`, /Other content/); - await expectToFail(() => ng(...serverBuildArgs)); -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts deleted file mode 100644 index 2402d2ed90cc..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as express from 'express'; -import { resolve } from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { - copyFile, - expectFileToExist, - expectFileToMatch, - replaceInFile, - writeFile, -} from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { readNgVersion } from '../../utils/version'; - -export default async function() { - // TEMP: disable pending i18n updates - // TODO: when re-enabling, use setupI18nConfig and helpers like other i18n tests. - return; - - let localizeVersion = '@angular/localize@' + readNgVersion(); - if (getGlobalVariable('argv')['ng-snapshots']) { - localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; - } - await installPackage(localizeVersion); - - let serviceWorkerVersion = '@angular/service-worker@' + readNgVersion(); - if (getGlobalVariable('argv')['ng-snapshots']) { - serviceWorkerVersion = require('../../ng-snapshot/package.json').dependencies[ - '@angular/service-worker' - ]; - } - await installPackage(serviceWorkerVersion); - - await updateJsonFile('tsconfig.json', config => { - config.compilerOptions.target = 'es2015'; - if (!config.angularCompilerOptions) { - config.angularCompilerOptions = {}; - } - config.angularCompilerOptions.disableTypeScriptVersionCheck = true; - }); - - const baseDir = 'dist/test-project'; - - // Set configurations for each locale. - const langTranslations = [ - { lang: 'en-US', translation: 'Hello i18n!' }, - { lang: 'fr', translation: 'Bonjour i18n!' }, - ]; - - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - const appArchitect = appProject.architect || appProject.targets; - const serveConfigs = appArchitect['serve'].configurations; - const e2eConfigs = appArchitect['e2e'].configurations; - - // Make default builds prod. - appArchitect['build'].options.optimization = true; - appArchitect['build'].options.buildOptimizer = true; - appArchitect['build'].options.aot = true; - appArchitect['build'].options.fileReplacements = [ - { - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts', - }, - ]; - - // Enable service worker - appArchitect['build'].options.serviceWorker = true; - - // Enable localization for all locales - // appArchitect['build'].options.localize = true; - - // Add locale definitions to the project - // tslint:disable-next-line: no-any - const i18n: Record = (appProject.i18n = { locales: {} }); - for (const { lang } of langTranslations) { - if (lang == 'en-US') { - i18n.sourceLocale = lang; - } else { - i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; - } - serveConfigs[lang] = { browserTarget: `test-project:build:${lang}` }; - e2eConfigs[lang] = { - specs: [`./src/app.${lang}.e2e-spec.ts`], - devServerTarget: `test-project:serve:${lang}`, - }; - } - }); - - // Add service worker source configuration - const manifest = { - 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/**', '/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)'], - }, - }, - ], - }; - await writeFile('ngsw-config.json', JSON.stringify(manifest)); - - // Add a translatable element. - await writeFile( - 'src/app/app.component.html', - '

Hello i18n!

', - ); - - // Extract the translation messages and copy them for each language. - await ng('extract-i18n', '--output-path=src/locale'); - await expectFileToExist('src/locale/messages.xlf'); - await expectFileToMatch('src/locale/messages.xlf', `source-language="en-US"`); - await expectFileToMatch('src/locale/messages.xlf', `An introduction header for this sample`); - - for (const { lang, translation } of langTranslations) { - if (lang != 'en-US') { - await copyFile('src/locale/messages.xlf', `src/locale/messages.${lang}.xlf`); - await replaceInFile( - `src/locale/messages.${lang}.xlf`, - 'source-language="en-US"', - `source-language="en-US" target-language="${lang}"`, - ); - await replaceInFile( - `src/locale/messages.${lang}.xlf`, - 'Hello i18n!', - `Hello i18n!\n${translation}`, - ); - } - } - - // Build each locale and verify the output. - await ng('build', '--i18n-missing-translation', 'error'); - for (const { lang, translation } of langTranslations) { - await expectFileToMatch(`${baseDir}/${lang}/main-es5.js`, translation); - await expectFileToMatch(`${baseDir}/${lang}/main-es2015.js`, translation); - await expectToFail(() => expectFileToMatch(`${baseDir}/${lang}/main-es5.js`, '$localize`')); - await expectToFail(() => expectFileToMatch(`${baseDir}/${lang}/main-es2015.js`, '$localize`')); - await expectFileToMatch(`${baseDir}/${lang}/main-es5.js`, lang); - await expectFileToMatch(`${baseDir}/${lang}/main-es2015.js`, lang); - - // Expect service worker configuration to be present - await expectFileToExist(`${baseDir}/${lang}/ngsw.json`); - - // Ivy i18n doesn't yet work with `ng serve` so we must use a separate server. - const app = express(); - app.use(express.static(resolve(baseDir, lang))); - const server = app.listen(4200, 'localhost'); - try { - // Add E2E test for locale - await writeFile( - 'e2e/src/app.e2e-spec.ts', - ` - import { browser, logging, element, by } from 'protractor'; - describe('workspace-project App', () => { - it('should display welcome message', () => { - browser.get(browser.baseUrl); - expect(element(by.css('h1')).getText()).toEqual('${translation}'); - }); - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - } as logging.Entry)); - }); - }); - `, - ); - - // Execute without a devserver. - await ng('e2e', '--devServerTarget='); - } finally { - server.close(); - } - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sourcelocale.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sourcelocale.ts deleted file mode 100644 index 18fa57f0fb0d..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sourcelocale.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { expectFileToMatch } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { langTranslations, setupI18nConfig } from './setup'; - -export default async function() { - // Setup i18n tests and config. - await setupI18nConfig(); - - // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - // tslint:disable-next-line: no-any - const i18n: Record = appProject.i18n; - - i18n.sourceLocale = 'fr'; - - delete i18n.locales['fr']; - }); - - // Build each locale and verify the output. - await ng('build', '--configuration=development'); - for (const { lang, outputPath } of langTranslations) { - // does not exist in this test due to the source locale change - if (lang === 'en-US') { - continue; - } - - await expectFileToMatch(`${outputPath}/vendor.js`, lang); - - // Verify the locale data is registered using the global files - await expectFileToMatch(`${outputPath}/vendor.js`, '.ng.common.locales'); - - // Verify the HTML lang attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); - } -} diff --git a/tests/legacy-cli/e2e/tests/i18n/setup.ts b/tests/legacy-cli/e2e/tests/i18n/setup.ts deleted file mode 100644 index efc957412df5..000000000000 --- a/tests/legacy-cli/e2e/tests/i18n/setup.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as express from 'express'; -import { resolve } from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { appendToFile, copyFile, expectFileToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { readNgVersion } from '../../utils/version'; - -// Configurations for each locale. -export const baseDir = 'dist/test-project'; -export const langTranslations = [ - { - lang: 'en-US', outputPath: `${baseDir}/en-US`, - translation: { - helloPartial: 'Hello', - hello: 'Hello i18n!', - plural: 'Updated 3 minutes ago', - date: 'January', - }, - }, - { - lang: 'fr', outputPath: `${baseDir}/fr`, - translation: { - helloPartial: 'Bonjour', - hello: 'Bonjour i18n!', - plural: 'Mis à jour il y a 3 minutes', - date: 'janvier', - }, - translationReplacements: [ - ['Hello', 'Bonjour'], - ['Updated', 'Mis à jour'], - ['just now', 'juste maintenant'], - ['one minute ago', 'il y a une minute'], - [/other {/g, 'other {il y a '], - ['minutes ago', 'minutes'], - ], - }, - { - lang: 'de', outputPath: `${baseDir}/de`, - translation: { - helloPartial: 'Hallo', - hello: 'Hallo i18n!', - plural: 'Aktualisiert vor 3 Minuten', - date: 'Januar', - }, - translationReplacements: [ - ['Hello', 'Hallo'], - ['Updated', 'Aktualisiert'], - ['just now', 'gerade jetzt'], - ['one minute ago', 'vor einer Minute'], - [/other {/g, 'other {vor '], - ['minutes ago', 'Minuten'], - ], - }, -]; -export const sourceLocale = langTranslations[0].lang; - -export const externalServer = (outputPath: string, baseUrl = '/') => { - const app = express(); - app.use(baseUrl, express.static(resolve(outputPath))); - - // call .close() on the return value to close the server. - return app.listen(4200, 'localhost'); -}; - -export const formats = { - 'xlf': { - ext: 'xlf', - sourceCheck: 'source-language="en-US"', - replacements: [ - [/source/g, 'target'], - ], - }, - 'xlf2': { - ext: 'xlf', - sourceCheck: 'srcLang="en-US"', - replacements: [ - [/source/g, 'target'], - ], - }, - 'xmb': { - ext: 'xmb', - sourceCheck: '.*?<\/source>/g, ''], - ], - }, - 'json': { - ext: 'json', - sourceCheck: '"locale": "en-US"', - replacements: [ - ], - }, - 'arb': { - ext: 'arb', - sourceCheck: '"@@locale": "en-US"', - replacements: [ - ], - }, -}; - -export async function setupI18nConfig(format: keyof typeof formats = 'xlf') { - // Add component with i18n content, both translations and localeData (plural, dates). - await writeFile('src/app/app.component.ts', ` - import { Component, Inject, LOCALE_ID } from '@angular/core'; - @Component({ - selector: 'app-root', - templateUrl: './app.component.html' - }) - export class AppComponent { - constructor(@Inject(LOCALE_ID) public locale: string) { } - title = 'i18n'; - jan = new Date(2000, 0, 1); - minutes = 3; - } - `); - await writeFile(`src/app/app.component.html`, ` -

Hello {{ title }}!

-

{{ locale }}

-

{{ jan | date : 'LLLL' }}

-

Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}

- `); - - // Add a dynamic import to ensure syntax is supported - // ng serve support: https://github.com/angular/angular-cli/issues/16248 - await writeFile('src/app/dynamic.ts', `export const abc = 5;`); - await appendToFile('src/app/app.component.ts', ` - (async () => { await import('./dynamic'); })(); - `); - - // Add e2e specs for each lang. - for (const { lang, translation } of langTranslations) { - await writeFile(`./e2e/src/app.${lang}.e2e-spec.ts`, ` - import { browser, logging, element, by } from 'protractor'; - - describe('workspace-project App', () => { - const getParagraph = async (name: string) => element(by.css('app-root p#' + name)).getText(); - beforeEach(() => browser.get(browser.baseUrl)); - afterEach(async () => { - // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - } as logging.Entry)); - }); - - it('should display welcome message', async () => - expect(await getParagraph('hello')).toEqual('${translation.hello}')); - - it('should display locale', async () => - expect(await getParagraph('locale')).toEqual('${lang}')); - - it('should display localized date', async () => - expect(await getParagraph('date')).toEqual('${translation.date}')); - - it('should display pluralized message', async () => - expect(await getParagraph('plural')).toEqual('${translation.plural}')); - }); - `); - } - - // Update angular.json to build, serve, and test each locale. - await updateJsonFile('angular.json', workspaceJson => { - const appProject = workspaceJson.projects['test-project']; - const appArchitect = workspaceJson.projects['test-project'].architect; - const buildConfigs = appArchitect['build'].configurations; - const serveConfigs = appArchitect['serve'].configurations; - const e2eConfigs = appArchitect['e2e'].configurations; - - appArchitect['build'].defaultConfiguration = undefined; - - // Always error on missing translations. - appArchitect['build'].options.optimization = true; - appArchitect['build'].options.buildOptimizer = true; - appArchitect['build'].options.aot = true; - appArchitect['build'].options.fileReplacements = [{ - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts', - }]; - appArchitect['build'].options.i18nMissingTranslation = 'error'; - appArchitect['build'].options.vendorChunk = true; - appArchitect['build'].options.sourceMap = true; - appArchitect['build'].options.outputHashing = 'none'; - - // Enable localization for all locales - appArchitect['build'].options.localize = true; - - // Add i18n config items (app, build, serve, e2e). - // tslint:disable-next-line: no-any - const i18n: Record = (appProject.i18n = { locales: {} }); - for (const { lang } of langTranslations) { - if (lang === sourceLocale) { - i18n.sourceLocale = lang; - } else { - i18n.locales[lang] = `src/locale/messages.${lang}.${formats[format].ext}`; - } - - buildConfigs[lang] = { localize: [lang] }; - - serveConfigs[lang] = { browserTarget: `test-project:build:${lang}` }; - e2eConfigs[lang] = { - specs: [`./src/app.${lang}.e2e-spec.ts`], - devServerTarget: `test-project:serve:${lang}`, - }; - } - }); - -// Install the localize package if using ivy - let localizeVersion = '@angular/localize@' + readNgVersion(); - if (getGlobalVariable('argv')['ng-snapshots']) { - localizeVersion = require('../../ng-snapshot/package.json').dependencies['@angular/localize']; - } - await installPackage(localizeVersion); - - // Extract the translation messages. - await ng( - 'extract-i18n', - '--output-path=src/locale', - `--format=${format}`, - ); - const translationFile = `src/locale/messages.${formats[format].ext}`; - await expectFileToExist(translationFile); - await expectFileToMatch(translationFile, formats[format].sourceCheck); - - if (format !== 'json') { - await expectFileToMatch(translationFile, `An introduction header for this sample`); - } - - // Make translations for each language. - for (const { lang, translationReplacements } of langTranslations) { - if (lang != sourceLocale) { - await copyFile(translationFile, `src/locale/messages.${lang}.${formats[format].ext}`); - for (const replacements of translationReplacements) { - await replaceInFile( - `src/locale/messages.${lang}.${formats[format].ext}`, - new RegExp(replacements[0], 'g'), - replacements[1] as string, - ); - } - for (const replacement of formats[format].replacements) { - await replaceInFile( - `src/locale/messages.${lang}.${formats[format].ext}`, - new RegExp(replacement[0], 'g'), - replacement[1] as string, - ); - } - } - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/ask-analytics-command.ts b/tests/legacy-cli/e2e/tests/misc/ask-analytics-command.ts deleted file mode 100644 index d571b38cb671..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/ask-analytics-command.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { execWithEnv, killAllProcesses, waitForAnyProcessOutputToMatch } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function() { - try { - // Execute a command with TTY force enabled - const execution = execWithEnv('ng', ['version'], { - ...process.env, - NG_FORCE_TTY: '1', - NG_CLI_ANALYTICS: 'ci', - }); - - // Check if the prompt is shown - await waitForAnyProcessOutputToMatch(/Would you like to share anonymous usage data/); - } finally { - killAllProcesses(); - } - - try { - // Execute a command with TTY force enabled - const execution = execWithEnv('ng', ['version'], { - ...process.env, - NG_FORCE_TTY: '1', - NG_CLI_ANALYTICS: 'false', - }); - - // Check if the prompt is shown - await expectToFail(() => - waitForAnyProcessOutputToMatch(/Would you like to share anonymous usage data/, 5), - ); - } finally { - killAllProcesses(); - } - - // Should not show a prompt when using update - try { - // Execute a command with TTY force enabled - const execution = execWithEnv('ng', ['update'], { - ...process.env, - NG_FORCE_TTY: '1', - NG_CLI_ANALYTICS: 'ci', - }); - - // Check if the prompt is shown - await expectToFail(() => - waitForAnyProcessOutputToMatch(/Would you like to share anonymous usage data/, 5), - ); - } finally { - killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/ask-analytics-install.ts b/tests/legacy-cli/e2e/tests/misc/ask-analytics-install.ts deleted file mode 100644 index 2731caf46a0f..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/ask-analytics-install.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createDir, rimraf } from '../../utils/fs'; -import { - execWithEnv, - killAllProcesses, - waitForAnyProcessOutputToMatch, -} from '../../utils/process'; - -export default async function() { - // Create a temporary directory to install the CLI - await createDir('../ask-analytics'); - const cwd = process.cwd(); - process.chdir('../ask-analytics'); - - try { - // Install the CLI with TTY force enabled - const execution = execWithEnv( - 'npm', - ['install', '@angular/cli'], - { ...process.env, 'NG_FORCE_TTY': '1' }, - ); - - // Check if the prompt is shown - await waitForAnyProcessOutputToMatch(/Would you like to share anonymous usage data/, 60000); - - } finally { - killAllProcesses(); - - // Cleanup - process.chdir(cwd); - await rimraf('../ask-analytics'); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/browsers.ts b/tests/legacy-cli/e2e/tests/misc/browsers.ts deleted file mode 100644 index fd221cca8d90..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/browsers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as express from 'express'; -import * as path from 'path'; -import { copyProjectAsset } from '../../utils/assets'; -import { getGlobalVariable } from '../../utils/env'; -import { replaceInFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - if (!process.env['E2E_BROWSERS']) { - return; - } - - // Ensure SauceLabs configuration - if (!process.env['SAUCE_USERNAME'] || !process.env['SAUCE_ACCESS_KEY']) { - throw new Error('SauceLabs is not configured.'); - } - - await replaceInFile( - '.browserslistrc', - 'not IE 11', - 'IE 11', - ); - - // Workaround for https://github.com/angular/angular/issues/32192 - await replaceInFile( - 'src/app/app.component.html', - /class="material-icons"/g, - '', - ); - - await ng('build'); - - // Add Protractor configuration - await copyProjectAsset('protractor-saucelabs.conf.js', 'e2e/protractor-saucelabs.conf.js'); - - // Remove browser log checks as they are only supported with the chrome webdriver - await replaceInFile( - 'e2e/src/app.e2e-spec.ts', - 'await browser.manage().logs().get(logging.Type.BROWSER)', - '[] as any', - ); - - // Workaround defect in getText WebDriver implementation for Safari/Edge - // Leading and trailing space is not removed - await replaceInFile( - 'e2e/src/app.e2e-spec.ts', - 'await page.getTitleText()', - '(await page.getTitleText()).trim()', - ); - - // Setup server - const app = express(); - app.use(express.static(path.resolve('dist/test-project'))); - const server = app.listen(2000, 'localhost'); - - try { - // Execute application's E2E tests with SauceLabs - await ng( - 'e2e', - 'test-project', - '--protractorConfig=e2e/protractor-saucelabs.conf.js', - '--devServerTarget=', - ); - } finally { - server.close(); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/circular-dependency.ts b/tests/legacy-cli/e2e/tests/misc/circular-dependency.ts deleted file mode 100644 index 0b1e4c8e49d2..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/circular-dependency.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { prependToFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default async function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - await prependToFile('src/app/app.component.ts', - `import { AppModule } from './app.module'; console.log(AppModule);`); - const { stderr } = await ng('build', '--show-circular-dependencies', '--configuration=development'); - if (!stderr.match(/Warning: Circular dependency detected/)) { - throw new Error('Expected to have circular dependency warning in output.'); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/common-async.ts b/tests/legacy-cli/e2e/tests/misc/common-async.ts deleted file mode 100644 index 90cf4a0227e3..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/common-async.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {readdirSync} from 'fs'; -import {oneLine} from 'common-tags'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import {appendToFile, expectFileToExist, prependToFile, replaceInFile} from '../../utils/fs'; -import {expectToFail} from '../../utils/utils'; - - -export default function() { - // TODO(architect): The common chunk seems to have a different name in devkit/build-angular. - // Investigate, validate, then delete this test. - return; - - let oldNumberOfFiles = 0; - return Promise.resolve() - .then(() => ng('build')) - .then(() => oldNumberOfFiles = readdirSync('dist/test-project').length) - .then(() => ng('generate', 'module', 'lazyA', '--routing')) - .then(() => ng('generate', 'module', 'lazyB', '--routing')) - .then(() => prependToFile('src/app/app.module.ts', ` - import { RouterModule } from '@angular/router'; - `)) - .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ - RouterModule.forRoot([{ path: "lazyA", loadChildren: "./lazy-a/lazy-a.module#LazyAModule" }]), - RouterModule.forRoot([{ path: "lazyB", loadChildren: "./lazy-b/lazy-b.module#LazyBModule" }]), - `)) - .then(() => ng('build')) - .then(() => readdirSync('dist').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles >= currentNumberOfDistFiles) { - throw new Error('A bundle for the lazy module was not created.'); - } - oldNumberOfFiles = currentNumberOfDistFiles; - }) - .then(() => installPackage('moment')) - .then(() => appendToFile('src/app/lazy-a/lazy-a.module.ts', ` - import * as moment from 'moment'; - console.log(moment); - `)) - .then(() => ng('build')) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles != currentNumberOfDistFiles) { - throw new Error('The build contains a different number of files.'); - } - }) - .then(() => appendToFile('src/app/lazy-b/lazy-b.module.ts', ` - import * as moment from 'moment'; - console.log(moment); - `)) - .then(() => ng('build')) - .then(() => expectFileToExist('dist/test-project/common.chunk.js')) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles >= currentNumberOfDistFiles) { - throw new Error(oneLine`The build contains the wrong number of files. - The test for 'dist/test-project/common.chunk.js' to exist should have failed.`); - } - oldNumberOfFiles = currentNumberOfDistFiles; - }) - .then(() => ng('build', '--no-common-chunk')) - .then(() => expectToFail(() => expectFileToExist('dist/test-project/common.chunk.js'))) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles <= currentNumberOfDistFiles) { - throw new Error(oneLine`The build contains the wrong number of files. - The test for 'dist/test-project/common.chunk.js' not to exist should have failed.`); - } - }) - // Check for AoT and lazy routes. - .then(() => ng('build', '--aot')) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles != currentNumberOfDistFiles) { - throw new Error('AoT build contains a different number of files.'); - } - }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/coverage.ts b/tests/legacy-cli/e2e/tests/misc/coverage.ts deleted file mode 100644 index af42921fbf76..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/coverage.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {expectFileToExist, expectFileToMatch} from '../../utils/fs'; -import {updateJsonFile} from '../../utils/project'; -import {expectToFail} from '../../utils/utils'; -import {ng} from '../../utils/process'; - - -export default function () { - // TODO(architect): This test is broken in devkit/build-angular, istanbul and - // istanbul-instrumenter-loader are missing from the dependencies. - return; - - return ng('test', '--watch=false', '--code-coverage') - .then(output => expect(output.stdout).toContain('Coverage summary')) - .then(() => expectFileToExist('coverage/src/app')) - .then(() => expectFileToExist('coverage/lcov.info')) - // Verify code coverage exclude work - .then(() => expectFileToMatch('coverage/lcov.info', 'polyfills.ts')) - .then(() => expectFileToMatch('coverage/lcov.info', 'test.ts')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.test.options.codeCoverageExclude = [ - 'src/polyfills.ts', - '**/test.ts', - ]; - })) - .then(() => ng('test', '--watch=false', '--code-coverage')) - .then(() => expectToFail(() => expectFileToMatch('coverage/lcov.info', 'polyfills.ts'))) - .then(() => expectToFail(() => expectFileToMatch('coverage/lcov.info', 'test.ts'))); -} diff --git a/tests/legacy-cli/e2e/tests/misc/dedupe-duplicate-modules.ts b/tests/legacy-cli/e2e/tests/misc/dedupe-duplicate-modules.ts deleted file mode 100644 index 5d4cb01ac606..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/dedupe-duplicate-modules.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expectFileToMatch, writeFile } from '../../utils/fs'; -import { installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - // Force duplicate modules - await updateJsonFile('package.json', json => { - json.dependencies = { - ...json.dependencies, - 'tslib': '2.0.0', - 'tslib-1': 'npm:tslib@1.13.0', - 'tslib-1-copy': 'npm:tslib@1.13.0', - }; - }); - - await installWorkspacePackages(); - - await writeFile('./src/main.ts', - ` - import { __assign as __assign_0 } from 'tslib'; - import { __assign as __assign_1 } from 'tslib-1'; - import { __assign as __assign_2 } from 'tslib-1-copy'; - - console.log({ - __assign_0, - __assign_1, - __assign_2, - }) - `); - - const { stderr } = await ng('build', '--verbose', '--no-vendor-chunk', '--no-progress', '--configuration=development'); - const outFile = 'dist/test-project/main.js'; - - if (/\[DedupeModuleResolvePlugin\]:.+tslib-1-copy.+ -> .+tslib-1.+/.test(stderr)) { - await expectFileToMatch(outFile, './node_modules/tslib-1/tslib.es6.js'); - await expectToFail(() => expectFileToMatch(outFile, './node_modules/tslib-1-copy/tslib.es6.js')); - } else if (/\[DedupeModuleResolvePlugin\]:.+tslib-1.+ -> .+tslib-1-copy.+/.test(stderr)) { - await expectFileToMatch(outFile, './node_modules/tslib-1-copy/tslib.es6.js'); - await expectToFail(() => expectFileToMatch(outFile, './node_modules/tslib-1/tslib.es6.js')); - } else { - console.error(`\n\n\n${stderr}\n\n\n`); - throw new Error('Expected stderr to contain [DedupeModuleResolvePlugin] log for tslib.'); - } - - await expectFileToMatch(outFile, './node_modules/tslib/tslib.es6.js'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/e2e-host.ts b/tests/legacy-cli/e2e/tests/misc/e2e-host.ts deleted file mode 100644 index d5ae4de1a20c..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/e2e-host.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as os from 'os'; -import { killAllProcesses, ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - const interfaces = [].concat.apply([], Object.values(os.networkInterfaces())); - let host = ''; - for (const { family, address, internal } of interfaces) { - if (family === 'IPv4' && !internal) { - host = address; - break; - } - } - - try { - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.serve.options = appArchitect.serve.options || {}; - appArchitect.serve.options.port = 8888; - appArchitect.serve.options.host = host; - }); - - await ng('e2e'); - - await ng('e2e', '--host', host); - } finally { - await killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts b/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts deleted file mode 100644 index ef319e184249..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/es2015-nometa.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prependToFile, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function() { - // Ensure an ES2015 build is used in test - await writeFile('.browserslistrc', 'Chrome 65'); - - await ng('generate', 'service', 'user'); - - // Update the application to use the new service - await prependToFile( - 'src/app/app.component.ts', - 'import { UserService } from \'./user.service\';', - ); - - await replaceInFile( - 'src/app/app.component.ts', - 'export class AppComponent {', - 'export class AppComponent {\n constructor(user: UserService) {}', - ); - - // Execute the application's tests with emitDecoratorMetadata disabled (default) - await ng('test', '--no-watch'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/es5-polyfills.ts b/tests/legacy-cli/e2e/tests/misc/es5-polyfills.ts deleted file mode 100644 index 417c2dd8e69a..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/es5-polyfills.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { oneLineTrim } from 'common-tags'; -import { expectFileNotToExist, expectFileToMatch, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - await updateJsonFile('tsconfig.json', configJson => { - const compilerOptions = configJson['compilerOptions']; - compilerOptions['target'] = 'es5'; - }); - - await writeFile('.browserslistrc', 'last 2 Chrome versions'); - await ng('build', '--configuration=development'); - await expectFileNotToExist('dist/test-project/polyfills-es5.js'); - await expectFileToMatch('dist/test-project/index.html', oneLineTrim` - - - - - `); - - await writeFile('.browserslistrc', 'IE 10'); - await ng('build', '--configuration=development'); - await expectFileToMatch('dist/test-project/polyfills-es5.js', 'core-js'); - await expectFileToMatch('dist/test-project/index.html', oneLineTrim` - - - - - - `); -} diff --git a/tests/legacy-cli/e2e/tests/misc/fallback.ts b/tests/legacy-cli/e2e/tests/misc/fallback.ts deleted file mode 100644 index 1c8d1ca56ea1..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/fallback.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { request } from '../../utils/http'; -import { killAllProcesses } from '../../utils/process'; -import { ngServe } from '../../utils/project'; -import { updateJsonFile } from '../../utils/project'; -import { moveFile } from '../../utils/fs'; - - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - // should fallback to config.app[0].index (index.html by default) - return Promise.resolve() - .then(() => ngServe()) - .then(() => request('http://localhost:4200/')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - // should correctly fallback to a changed index - .then(() => moveFile('src/index.html', 'src/not-index.html')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.index = 'src/not-index.html'; - })) - .then(() => ngServe()) - .then(() => request('http://localhost:4200/')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts b/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts deleted file mode 100644 index 497f60aab160..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/forwardref-es2015.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { appendToFile, replaceInFile, writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function() { - // Ensure an ES2015 build is used in test - await writeFile('.browserslistrc', 'Chrome 65'); - - // Update the application to use a forward reference - await replaceInFile( - 'src/app/app.component.ts', - 'import { Component } from \'@angular/core\';', - 'import { Component, Inject, Injectable, forwardRef } from \'@angular/core\';', - ); - await appendToFile('src/app/app.component.ts', '\n@Injectable() export class Lock { }\n'); - await replaceInFile( - 'src/app/app.component.ts', - 'export class AppComponent {', - 'export class AppComponent {\n constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {}', - ); - - // Update the application's unit tests to include the new injectable - await replaceInFile( - 'src/app/app.component.spec.ts', - 'import { AppComponent } from \'./app.component\';', - 'import { AppComponent, Lock } from \'./app.component\';', - ); - await replaceInFile( - 'src/app/app.component.spec.ts', - 'TestBed.configureTestingModule({', - 'TestBed.configureTestingModule({ providers: [Lock],', - ); - - // Execute the application's tests with emitDecoratorMetadata disabled (default) - await ng('test', '--no-watch'); - - // Turn on emitDecoratorMetadata - await replaceInFile( - 'tsconfig.json', - '"experimentalDecorators": true', - '"experimentalDecorators": true, "emitDecoratorMetadata": true', - ); - - // Execute the application's tests with emitDecoratorMetadata enabled - await expectToFail(() => ng('test', '--no-watch')); -} diff --git a/tests/legacy-cli/e2e/tests/misc/http-headers.ts b/tests/legacy-cli/e2e/tests/misc/http-headers.ts deleted file mode 100644 index 23ebaaa2b5a0..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/http-headers.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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 { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - // This test ensures that ng e2e serves the HTTP headers that are configured - // in the 'headers' field of the serve options. We do this by serving the - // strictest possible CSP headers (default-src 'none') which blocks loading of - // any resources (including scripts, styles and images) and should cause ng - // e2e to fail with a CSP-related error, which is asserted below. - - await updateJsonFile('angular.json', (json) => { - const serve = json['projects']['test-project']['architect']['serve']; - if (!serve['options']) serve['options'] = {}; - serve['options']['headers'] = { - 'Content-Security-Policy': "default-src 'none'", - }; - }); - - let errorMessage = null; - try { - await ng('e2e'); - } catch (error) { - errorMessage = error.message; - } - - if (!errorMessage) { - throw new Error( - 'Application loaded successfully, indicating that the CSP headers were not served.', - ); - } - if (!errorMessage.match(/Refused to load/)) { - throw new Error('Expected to see CSP loading failure in error logs.'); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts b/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts deleted file mode 100644 index 4b8c05f04737..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { ng, silentNpm } from '../../utils/process'; -import { installPackage, uninstallPackage } from '../../utils/packages'; -import { isPrereleaseCli } from '../../utils/project'; - -export default async function () { - // Must publish old version to local registry to allow install. This is especially important - // for release commits as npm will try to request tooling packages that are not on the npm registry yet - const { stdout: stdoutPack1 } = await silentNpm( - 'pack', - '@schematics/angular@7', - '--registry=https://registry.npmjs.org', - ); - await silentNpm('publish', stdoutPack1.trim(), '--tag=outdated'); - const { stdout: stdoutPack2 } = await silentNpm( - 'pack', - '@angular-devkit/core@7', - '--registry=https://registry.npmjs.org', - ); - await silentNpm('publish', stdoutPack2.trim(), '--tag=outdated'); - const { stdout: stdoutPack3 } = await silentNpm( - 'pack', - '@angular-devkit/schematics@7', - '--registry=https://registry.npmjs.org', - ); - await silentNpm('publish', stdoutPack3.trim(), '--tag=outdated'); - - // Install outdated and incompatible version - await installPackage('@schematics/angular@7'); - - const tag = (await isPrereleaseCli()) ? '@next' : ''; - await ng('add', `@angular/material${tag}`, '--skip-confirmation'); - await expectFileToMatch('package.json', /@angular\/material/); - - // Clean up existing cdk package - // Not doing so can cause adding material to fail if an incompatible cdk is present - await uninstallPackage('@angular/cdk'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/karma-error-paths.ts b/tests/legacy-cli/e2e/tests/misc/karma-error-paths.ts deleted file mode 100644 index d8916ab6ea75..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/karma-error-paths.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - await writeMultipleFiles({ - 'src/app/app.component.spec.ts': ` - describe('AppComponent', () => { - it('failing test', () => { - expect('1').toEqual('2'); - }); - }); - `, - }); - - const { message } = await expectToFail(() => ng('test', '--no-watch')); - if (message.includes('_karma_webpack_')) { - throw new Error(`Didn't expect logs to server address and webpack scheme.\n${message}`); - } - - if (!message.includes('(src/app/app.component.spec.ts:4:25)')) { - throw new Error(`Expected logs to contain relative path to (src/app/app.component.spec.ts:4:25)\n${message}`); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/lazy-module.ts b/tests/legacy-cli/e2e/tests/misc/lazy-module.ts deleted file mode 100644 index 320f4206de05..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/lazy-module.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {readdirSync} from 'fs'; -import { installPackage } from '../../utils/packages'; -import {ng} from '../../utils/process'; -import {appendToFile, writeFile, prependToFile, replaceInFile} from '../../utils/fs'; - - -export default function() { - let oldNumberOfFiles = 0; - return Promise.resolve() - .then(() => ng('build', '--configuration=development')) - .then(() => oldNumberOfFiles = readdirSync('dist').length) - .then(() => ng('generate', 'module', 'lazy', '--routing')) - .then(() => ng('generate', 'module', 'too/lazy', '--routing')) - .then(() => prependToFile('src/app/app.module.ts', ` - import { RouterModule } from '@angular/router'; - `)) - .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ - RouterModule.forRoot([{ path: "lazy", loadChildren: () => import('src/app/lazy/lazy.module').then(m => m.LazyModule) }]), - RouterModule.forRoot([{ path: "lazy1", loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) }]), - RouterModule.forRoot([{ path: "lazy2", loadChildren: () => import('./too/lazy/lazy.module').then(m => m.LazyModule) }]), - `)) - .then(() => ng('build', '--named-chunks', '--configuration=development')) - .then(() => readdirSync('dist/test-project')) - .then((distFiles) => { - const currentNumberOfDistFiles = distFiles.length; - if (oldNumberOfFiles >= currentNumberOfDistFiles) { - throw new Error('A bundle for the lazy module was not created.'); - } - oldNumberOfFiles = currentNumberOfDistFiles; - - if (!distFiles.includes('src_app_too_lazy_lazy_module_ts.js')) { - throw new Error('The lazy module chunk did not use a unique name.'); - } - }) - // verify 'import *' syntax doesn't break lazy modules - .then(() => installPackage('moment')) - .then(() => appendToFile('src/app/app.component.ts', ` - import * as moment from 'moment'; - console.log(moment); - `)) - .then(() => ng('build', '--configuration=development')) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles != currentNumberOfDistFiles) { - throw new Error('Bundles were not created after adding \'import *\'.'); - } - }) - .then(() => ng('build', '--no-named-chunks', '--configuration=development')) - .then(() => readdirSync('dist/test-project')) - .then((distFiles) => { - if (distFiles.includes('lazy-lazy-module.js') || distFiles.includes('too-lazy-lazy-module.js')) { - throw new Error('Lazy chunks shouldn\'t have a name but did.'); - } - }) - // Check for AoT and lazy routes. - .then(() => ng('build', '--aot', '--configuration=development')) - .then(() => readdirSync('dist/test-project').length) - .then(currentNumberOfDistFiles => { - if (oldNumberOfFiles != currentNumberOfDistFiles) { - throw new Error('AoT build contains a different number of files.'); - } - }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/loaders-resolution.ts b/tests/legacy-cli/e2e/tests/misc/loaders-resolution.ts deleted file mode 100644 index 7c011a1d7dba..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/loaders-resolution.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createDir, moveFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - await createDir('node_modules/@angular-devkit/build-angular/node_modules'); - await moveFile( - 'node_modules/@ngtools', - 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools' - ); - - await ng('build', '--configuration=development'); - - // Move it back. - await moveFile( - 'node_modules/@angular-devkit/build-angular/node_modules/@ngtools', - 'node_modules/@ngtools', - ); -} diff --git a/tests/legacy-cli/e2e/tests/misc/minimal-config.ts b/tests/legacy-cli/e2e/tests/misc/minimal-config.ts deleted file mode 100644 index a5ad29d11990..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/minimal-config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { writeFile, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default function () { - // TODO(architect): Figure out what a minimal config is for architect apps. - return; - - return Promise.resolve() - .then(() => writeFile('angular.json', JSON.stringify({ - apps: [{ - root: 'src', - main: 'main.ts', - scripts: [ - '../node_modules/core-js/client/shim.min.js', - '../node_modules/zone.js/dist/zone.js' - ] - }], - e2e: { protractor: { config: './protractor.conf.js' } } - }))) - .then(() => ng('e2e', 'test-project-e2e')) - .then(() => writeMultipleFiles({ - './src/script.js': ` - document.querySelector('app-root').innerHTML = '

app works!

'; - `, - './e2e/app.e2e-spec.ts': ` - import { browser, element, by } from 'protractor'; - - describe('minimal project App', function() { - it('should display message saying app works', () => { - browser.ignoreSynchronization = true; - browser.get('/'); - let el = element(by.css('app-root h1')).getText(); - expect(el).toEqual('app works!'); - }); - }); - `, - 'angular.json': JSON.stringify({ - apps: [{ - root: 'src', - scripts: ['./script.js'] - }], - e2e: { protractor: { config: './protractor.conf.js' } } - }), - })) - .then(() => ng('e2e', 'test-project-e2e')); -} diff --git a/tests/legacy-cli/e2e/tests/misc/module-resolution.ts b/tests/legacy-cli/e2e/tests/misc/module-resolution.ts deleted file mode 100644 index 5b812b6c7242..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/module-resolution.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { appendToFile, createDir, moveFile, prependToFile } from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - - -export default async function () { - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '*': ['./node_modules/*'], - }; - }); - await ng('build', '--configuration=development'); - - await createDir('xyz'); - await moveFile( - 'node_modules/@angular/common', - 'xyz/common', - ); - - await expectToFail(() => ng('build', '--configuration=development')); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '@angular/common': [ './xyz/common' ], - }; - }); - await ng('build', '--configuration=development'); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '*': ['./node_modules/*'], - '@angular/common': [ './xyz/common' ], - }; - }); - await ng('build', '--configuration=development'); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '@angular/common': [ './xyz/common' ], - '*': ['./node_modules/*'], - }; - }); - await ng('build', '--configuration=development'); - - await updateJsonFile('tsconfig.json', tsconfig => { - delete tsconfig.compilerOptions.paths; - }); - - await prependToFile('src/app/app.module.ts', 'import * as firebase from \'firebase\';'); - await appendToFile('src/app/app.module.ts', 'firebase.initializeApp({});'); - - await installPackage('firebase@3.7.8'); - await ng('build', '--aot', '--configuration=development'); - await ng('test', '--watch=false'); - - await installPackage('firebase@4.9.0'); - await ng('build', '--aot', '--configuration=development'); - await ng('test', '--watch=false'); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = {}; - }); - await ng('build', '--configuration=development'); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '@app/*': ['*'], - '@lib/*/test': ['*/test'], - }; - }); - await ng('build', '--configuration=development'); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '@firebase/polyfill': ['./node_modules/@firebase/polyfill/index.ts'], - }; - }); - await expectToFail(() => ng('build', '--configuration=development')); - - await updateJsonFile('tsconfig.json', tsconfig => { - tsconfig.compilerOptions.paths = { - '@firebase/polyfill*': ['./node_modules/@firebase/polyfill/index.ts'], - }; - }); - await expectToFail(() => ng('build', '--configuration=development')); -} diff --git a/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts b/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts deleted file mode 100644 index 5ac349343166..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/multiple-targets.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expectFileToExist } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - await ng('generate', 'app', 'secondary-app'); - - await updateJsonFile('angular.json', workspaceJson => { - workspaceJson.defaultProject = undefined; - }); - - await ng('build', 'secondary-app', '--configuration=development'); - - expectFileToExist('dist/secondary-app/index.html'); - expectFileToExist('dist/secondary-app/main.js'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/non-relative-module-resolution.ts b/tests/legacy-cli/e2e/tests/misc/non-relative-module-resolution.ts deleted file mode 100644 index 4d51c8bcae98..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/non-relative-module-resolution.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { prependToFile, writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; - - -export default async function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - await writeMultipleFiles({ - './src/app/foo.ts': ` - export const foo = 'fooo'; - `, - './src/app/bar.ts': ` - import { foo } from './foo'; - - console.log(foo); - ` - }), - - await prependToFile('src/app/app.module.ts', `import './bar';\n`); - - await ng('build', '--configuration=development'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/npm-7.ts b/tests/legacy-cli/e2e/tests/misc/npm-7.ts deleted file mode 100644 index c2bff8ad9d34..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/npm-7.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { rimraf, writeFile } from '../../utils/fs'; -import { getActivePackageManager } from '../../utils/packages'; -import { ng, npm } from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - -const warningText = 'npm version 7.5.6 or higher is recommended'; - -export default async function() { - // Only relevant with npm as a package manager - if (getActivePackageManager() !== 'npm') { - return; - } - - // Windows CI fails with permission errors when trying to replace npm - if (process.platform.startsWith('win')) { - return; - } - - const currentDirectory = process.cwd(); - try { - // Install version >=7.5.6 - await npm('install', '--global', 'npm@>=7.5.6'); - - // Ensure `ng update` does not show npm warning - const { stderr: stderrUpdate1 } = await ng('update'); - if (stderrUpdate1.includes(warningText)) { - throw new Error('ng update expected to not show npm version warning.'); - } - - // Install version <7.5.6 - await npm('install', '--global', 'npm@7.4.0'); - - // Ensure `ng add` shows npm warning - const { message: stderrAdd } = await expectToFail(() => ng('add')); - if (!stderrAdd.includes(warningText)) { - throw new Error('ng add expected to show npm version warning.'); - } - - // Ensure `ng update` shows npm warning - const { stderr: stderrUpdate2 } = await ng('update'); - if (!stderrUpdate2.includes(warningText)) { - throw new Error('ng update expected to show npm version warning.'); - } - - // Ensure `ng build` executes successfully - const { stderr: stderrBuild } = await ng('build', '--configuration=development'); - if (stderrBuild.includes(warningText)) { - throw new Error('ng build expected to not show npm version warning.'); - } - - // Ensure `ng new` shows npm warning - // Must be outside the project for `ng new` - process.chdir('..'); - const { message: stderrNew } = await expectToFail(() => ng('new')); - if (!stderrNew.includes(warningText)) { - throw new Error('ng new expected to show npm version warning.'); - } - - // Ensure `ng new --package-manager=npm` shows npm warning - const { message: stderrNewNpm } = await expectToFail(() => ng('new', '--package-manager=npm')); - if (!stderrNewNpm.includes(warningText)) { - throw new Error('ng new expected to show npm version warning.'); - } - - // Ensure `ng new --skip-install` executes successfully - const { stderr: stderrNewSkipInstall } = await ng('new', 'npm-seven-skip', '--skip-install'); - if (stderrNewSkipInstall.includes(warningText)) { - throw new Error('ng new --skip-install expected to not show npm version warning.'); - } - - // Ensure `ng new --package-manager=yarn` executes successfully - // Need an additional npmrc file since yarn does not use the NPM registry environment variable - const { stderr: stderrNewYarn } = await ng('new', 'npm-seven-yarn', '--package-manager=yarn'); - if (stderrNewYarn.includes(warningText)) { - throw new Error('ng new --package-manager=yarn expected to not show npm version warning.'); - } - } finally { - // Cleanup extra test projects - await rimraf('npm-seven-skip'); - await rimraf('npm-seven-yarn'); - - // Change directory back - process.chdir(currentDirectory); - - // Reset version back to 6.x - await npm('install', '--global', 'npm@6'); - } - -} diff --git a/tests/legacy-cli/e2e/tests/misc/npm-audit.ts b/tests/legacy-cli/e2e/tests/misc/npm-audit.ts deleted file mode 100644 index c96e17132e80..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/npm-audit.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { npm } from '../../utils/process'; - - -export default async function() { - try { - await npm('audit'); - } catch {} -} diff --git a/tests/legacy-cli/e2e/tests/misc/proxy-config.ts b/tests/legacy-cli/e2e/tests/misc/proxy-config.ts deleted file mode 100644 index ec5ce3d7101e..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/proxy-config.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as express from 'express'; -import * as http from 'http'; - -import {writeFile} from '../../utils/fs'; -import {request} from '../../utils/http'; -import {killAllProcesses, ng} from '../../utils/process'; -import {ngServe} from '../../utils/project'; -import {updateJsonFile} from '../../utils/project'; -import {expectToFail} from "../../utils/utils"; - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - // Create an express app that serves as a proxy. - const app = express(); - const server = http.createServer(app); - server.listen(0); - - app.set('port', server.address().port); - app.get('/api/test', function (req, res) { - res.send('TEST_API_RETURN'); - }); - - const backendHost = 'localhost'; - const backendPort = server.address().port; - const proxyServerUrl = `http://${backendHost}:${backendPort}`; - const proxyConfigFile = 'proxy.config.json'; - const proxyConfig = { - '/api/*': { - target: proxyServerUrl - } - }; - - return Promise.resolve() - .then(() => writeFile(proxyConfigFile, JSON.stringify(proxyConfig, null, 2))) - .then(() => ngServe('--proxy-config', proxyConfigFile)) - .then(() => request('http://localhost:4200/api/test')) - .then(body => { - if (!body.match(/TEST_API_RETURN/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - - // .then(() => updateJsonFile('angular.json', configJson => { - // const app = configJson.defaults; - // app.serve = { - // proxyConfig: proxyConfigFile - // }; - // })) - // .then(() => ngServe()) - // .then(() => request('http://localhost:4200/api/test')) - // .then(body => { - // if (!body.match(/TEST_API_RETURN/)) { - // throw new Error('Response does not match expected value.'); - // } - // }) - // .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - - .then(() => server.close(), (err) => { server.close(); throw err; }) - - // // A non-existing proxy file should error. - // .then(() => expectToFail(() => ng('serve', '--proxy-config', 'proxy.non-existent.json'))) - // .then(() => updateJsonFile('angular.json', configJson => { - // const app = configJson.defaults; - // app.serve = { - // proxyConfig: 'proxy.non-existent.json' - // }; - // })) - // .then(() => expectToFail(() => ng('serve'))); -} diff --git a/tests/legacy-cli/e2e/tests/misc/public-host.ts b/tests/legacy-cli/e2e/tests/misc/public-host.ts deleted file mode 100644 index faf63bd82316..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/public-host.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as os from 'os'; -import * as _ from 'lodash'; - -import { request } from '../../utils/http'; -import { killAllProcesses } from '../../utils/process'; -import { ngServe } from '../../utils/project'; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - const firstLocalIp = _(os.networkInterfaces()) - .values() - .flatten() - .filter({ family: 'IPv4', internal: false }) - .map('address') - .first(); - const publicHost = `${firstLocalIp}:4200`; - const localAddress = `http://${publicHost}`; - - return Promise.resolve() - // Disabling this test. Webpack Dev Server does not check the hots anymore when binding to - // numeric IP addresses. - // .then(() => ngServe('--host=0.0.0.0')) - // .then(() => request(localAddress)) - // .then(body => { - // if (!body.match(/Invalid Host header/)) { - // throw new Error('Response does not match expected value.'); - // } - // }) - // .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - .then(() => ngServe('--host=0.0.0.0', `--public-host=${publicHost}`)) - .then(() => request(localAddress)) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - .then(() => ngServe('--host=0.0.0.0', `--disable-host-check`)) - .then(() => request(localAddress)) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - .then(() => ngServe('--host=0.0.0.0', `--public-host=${localAddress}`)) - .then(() => request(localAddress)) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }) - .then(() => ngServe('--host=0.0.0.0', `--public-host=${firstLocalIp}`)) - .then(() => request(localAddress)) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/ssl-default.ts b/tests/legacy-cli/e2e/tests/misc/ssl-default.ts deleted file mode 100644 index e3b8cd2cfe4e..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/ssl-default.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { request } from '../../utils/http'; -import { killAllProcesses } from '../../utils/process'; -import { ngServe } from '../../utils/project'; - - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => ngServe('--ssl', 'true')) - .then(() => request('https://localhost:4200/')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts b/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts deleted file mode 100644 index a905d17322bf..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { request } from '../../utils/http'; -import { assetDir } from '../../utils/assets'; -import { killAllProcesses } from '../../utils/process'; -import { ngServe } from '../../utils/project'; - - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => ngServe( - '--ssl', 'true', - '--ssl-key', assetDir('ssl/server.key'), - '--ssl-cert', assetDir('ssl/server.crt') - )) - .then(() => request('https://localhost:4200/')) - .then(body => { - if (!body.match(/<\/app-root>/)) { - throw new Error('Response does not match expected value.'); - } - }) - .then(() => killAllProcesses(), (err) => { killAllProcesses(); throw err; }); - -} diff --git a/tests/legacy-cli/e2e/tests/misc/target-default-configuration.ts b/tests/legacy-cli/e2e/tests/misc/target-default-configuration.ts deleted file mode 100644 index dca271a838aa..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/target-default-configuration.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expectFileToExist } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; - -export default async function () { - await updateJsonFile('angular.json', workspace => { - const build = workspace.projects['test-project'].architect.build; - build.defaultConfiguration = undefined; - build.options = { - ...build.options, - optimization: false, - buildOptimizer: false, - outputHashing: 'none', - sourceMap: true, - }; - }); - - await ng('build'); - await expectFileToExist('dist/test-project/main.js'); - await expectFileToExist('dist/test-project/main.js.map'); - - // Add new configuration and set "defaultConfiguration" - await updateJsonFile('angular.json', workspace => { - const build = workspace.projects['test-project'].architect.build; - build.defaultConfiguration = 'foo'; - build.configurations.foo = { - sourceMap: false, - }; - }); - - await ng('build'); - await expectFileToExist('dist/test-project/main.js'); - await expectToFail(() => expectFileToExist('dist/test-project/main.js.map')); -} diff --git a/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts b/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts deleted file mode 100644 index 40a2b730c114..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { writeMultipleFiles } from '../../utils/fs'; -import { installWorkspacePackages } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default function () { - return updateJsonFile('package.json', packageJson => { - // Install ngrx - packageJson['dependencies']['@ngrx/effects'] = '^9.1.0'; - packageJson['dependencies']['@ngrx/schematics'] = '^9.1.0'; - packageJson['dependencies']['@ngrx/store'] = '^9.1.0'; - packageJson['dependencies']['@ngrx/store-devtools'] = '^9.1.0'; - }) - .then(() => installWorkspacePackages()) - // Create an app that uses ngrx decorators and has e2e tests. - .then(_ => writeMultipleFiles({ - './e2e/src/app.po.ts': ` - import { browser, by, element } from 'protractor'; - export class AppPage { - async navigateTo() { return browser.get('/'); } - getIncrementButton() { return element(by.buttonText('Increment')); } - getDecrementButton() { return element(by.buttonText('Decrement')); } - getResetButton() { return element(by.buttonText('Reset Counter')); } - async getCounter() { return element(by.xpath('/html/body/app-root/div/span')).getText(); } - } - `, - './e2e/src/app.e2e-spec.ts': ` - import { AppPage } from './app.po'; - - describe('workspace-project App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should operate counter', async () => { - await page.navigateTo(); - await page.getIncrementButton().click(); - await page.getIncrementButton().click(); - expect(await page.getCounter()).toEqual('2'); - await page.getDecrementButton().click(); - expect(await page.getCounter()).toEqual('1'); - await page.getResetButton().click(); - expect(await page.getCounter()).toEqual('0'); - }); - }); - `, - './src/app/app.component.ts': ` - import { Component } from '@angular/core'; - import { Store, select } from '@ngrx/store'; - import { Observable } from 'rxjs'; - import { INCREMENT, DECREMENT, RESET } from './counter.reducer'; - - interface AppState { - count: number; - } - - @Component({ - selector: 'app-root', - template: \` - -
Current Count: {{ count$ | async }}
- - - - \`, - }) - export class AppComponent { - count$: Observable; - - constructor(private store: Store) { - this.count$ = store.pipe(select(state => state.count)); - } - - increment() { - this.store.dispatch({ type: INCREMENT }); - } - - decrement() { - this.store.dispatch({ type: DECREMENT }); - } - - reset() { - this.store.dispatch({ type: RESET }); - } - } - `, - './src/app/app.effects.ts': ` - import { Injectable } from '@angular/core'; - import { Actions, Effect } from '@ngrx/effects'; - import { filter, map, tap } from 'rxjs/operators'; - - @Injectable() - export class AppEffects { - - @Effect() - mapper$ = this.actions$.pipe(map(() => ({ type: 'ANOTHER'})), filter(() => false)); - - @Effect({ dispatch: false }) - logger$ = this.actions$.pipe(tap(console.log)); - - constructor(private actions$: Actions) {} - } - `, - './src/app/app.module.ts': ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - - import { AppComponent } from './app.component'; - import { StoreModule } from '@ngrx/store'; - import { StoreDevtoolsModule } from '@ngrx/store-devtools'; - import { environment } from '../environments/environment'; - import { EffectsModule } from '@ngrx/effects'; - import { AppEffects } from './app.effects'; - import { counterReducer } from './counter.reducer'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - StoreModule.forRoot({ count: counterReducer }), - !environment.production ? StoreDevtoolsModule.instrument() : [], - EffectsModule.forRoot([AppEffects]) - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, - './src/app/counter.reducer.ts': ` - import { Action } from '@ngrx/store'; - - export const INCREMENT = 'INCREMENT'; - export const DECREMENT = 'DECREMENT'; - export const RESET = 'RESET'; - - const initialState = 0; - - export function counterReducer(state: number = initialState, action: Action) { - switch (action.type) { - case INCREMENT: - return state + 1; - - case DECREMENT: - return state - 1; - - case RESET: - return 0; - - default: - return state; - } - } - `, - })) - // Run the e2e tests against a prod build. - .then(() => ng('e2e', '--prod')); -} diff --git a/tests/legacy-cli/e2e/tests/misc/title.ts b/tests/legacy-cli/e2e/tests/misc/title.ts deleted file mode 100644 index 37f65e63c71d..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/title.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { execAndWaitForOutputToMatch, execWithEnv, killAllProcesses } from '../../utils/process'; - - -export default async function() { - if (process.platform.startsWith('win')) { - // "On Windows, process.title affects the console title, but not the name of the process in the task manager." - // https://stackoverflow.com/questions/44756196/how-to-change-the-node-js-process-name-on-windows-10#comment96259375_44756196 - return Promise.resolve(); - } - - try { - await execAndWaitForOutputToMatch('ng', ['build', '--configuration=development', '--watch'], /./); - - const output = await execWithEnv('ps', ['x'], { COLUMNS: '200' }); - - if (!output.stdout.match(/ng build --configuration=development --watch/)) { - throw new Error('Title of the process was not properly set.'); - } - } finally { - killAllProcesses(); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/universal-bundle-dependencies.ts b/tests/legacy-cli/e2e/tests/misc/universal-bundle-dependencies.ts deleted file mode 100644 index f00c1087589e..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/universal-bundle-dependencies.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as path from 'path'; -import { - createDir, - expectFileToMatch, - rimraf, - symlinkFile, - writeMultipleFiles, -} from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function() { - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect['server'] = { - builder: '@angular-devkit/build-angular:server', - options: { - bundleDependencies: false, - outputPath: 'dist/test-project-server', - main: 'src/main.server.ts', - tsConfig: 'tsconfig.server.json', - }, - }; - }); - - await createDir('./dummy-lib'); - - await writeMultipleFiles({ - './tsconfig.server.json': ` - { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../dist-server", - "baseUrl": "./", - "module": "commonjs", - "types": [] - }, - "include": [ - "src/main.server.ts" - ] - } - `, - './src/main.server.ts': ` - import { dummyVersion } from 'dummy-lib'; - console.log(dummyVersion); - `, - // create a dummy library - './dummy-lib/package.json': `{ - "name": "dummy-lib", - "version": "0.0.0", - "typings": "./main.d.ts", - "main": "./main.js" - }`, - './dummy-lib/main.js': 'export const dummyVersion = 1', - './dummy-lib/main.d.ts': 'export declare const dummyVersion = 1', - }); - - await symlinkFile(path.resolve('./dummy-lib'), path.resolve('./node_modules/dummy-lib'), 'dir'); - - await ng('run', 'test-project:server'); - // when preserve symlinks is true, it should not included node_modules in the bundle - await expectFileToMatch('dist/test-project-server/main.js', 'require("dummy-lib")'); - - // cleanup the package - await rimraf('node_modules/dummy-lib'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/update-git-clean-subdirectory.ts b/tests/legacy-cli/e2e/tests/misc/update-git-clean-subdirectory.ts deleted file mode 100644 index 8e2a3f668c65..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/update-git-clean-subdirectory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { createDir, writeFile } from '../../utils/fs'; -import { ng, silentGit } from '../../utils/process'; -import { prepareProjectForE2e } from '../../utils/project'; - -export default async function() { - process.chdir(getGlobalVariable('tmp-root')); - - await createDir('./subdirectory'); - process.chdir('./subdirectory'); - - await silentGit('init', '.'); - - await ng('new', 'subdirectory-test-project', '--skip-install'); - process.chdir('./subdirectory-test-project'); - await prepareProjectForE2e('subdirectory-test-project'); - - await writeFile('../added.ts', 'console.log(\'created\');\n'); - await silentGit('add', '../added.ts'); - - const { stderr } = await ng('update', '@angular/cli'); - if (stderr && stderr.includes('Repository is not clean.')) { - throw new Error('Expected clean repository'); - } -} diff --git a/tests/legacy-cli/e2e/tests/misc/update-help.ts b/tests/legacy-cli/e2e/tests/misc/update-help.ts deleted file mode 100644 index 2e98d9fd4a9d..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/update-help.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ng } from '../../utils/process'; - -export default function () { - return Promise.resolve() - .then(() => ng('update', '--help')) - .then(({ stdout }) => { - if (!/next/.test(stdout)) { - throw 'Update help should contain "next" option'; - } - }); -} diff --git a/tests/legacy-cli/e2e/tests/misc/version.ts b/tests/legacy-cli/e2e/tests/misc/version.ts deleted file mode 100644 index c39c3167bf0b..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/version.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { deleteFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; - -export default async function () { - const { stdout: commandOutput } = await ng('version'); - const { stdout: optionOutput } = await ng('--version'); - if (!optionOutput.includes('Angular CLI:')) { - throw new Error('version not displayed'); - } - - if (commandOutput !== optionOutput) { - throw new Error('version variants have differing output'); - } - - if (commandOutput.includes(process.versions.node + ' (Unsupported)')) { - throw new Error('Node version should not show unsupported entry'); - } - - if (commandOutput.includes('Warning: The current version of Node ')) { - throw new Error('Node support warning should not be shown'); - } - - // doesn't fail on a project with missing angular.json - await deleteFile('angular.json'); - await ng('version'); - - // Doesn't fail outside a project. - process.chdir('/'); - await ng('version'); -} diff --git a/tests/legacy-cli/e2e/tests/misc/workspace-verification.ts b/tests/legacy-cli/e2e/tests/misc/workspace-verification.ts deleted file mode 100644 index a9353edf7395..000000000000 --- a/tests/legacy-cli/e2e/tests/misc/workspace-verification.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {deleteFile} from '../../utils/fs'; -import {ng} from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; - - -export default function() { - return ng('generate', 'component', 'foo', '--dry-run') - .then(() => deleteFile('angular.json')) - // fails because it needs to be inside a project - // without a workspace file - .then(() => expectToFail(() => ng('generate', 'component', 'foo', '--dry-run'))) - .then(() => ng('version')); -} diff --git a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts b/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts deleted file mode 100644 index 1ee5f70b03ed..000000000000 --- a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { normalize } from 'path'; -import { createProjectFromAsset } from '../../../utils/assets'; -import { expectFileSizeToBeUnder, expectFileToMatch, replaceInFile } from '../../../utils/fs'; -import { exec } from '../../../utils/process'; - -export default async function (skipCleaning: () => void) { - const webpackCLIBin = normalize('node_modules/.bin/webpack-cli'); - - await createProjectFromAsset('webpack/test-app'); - - await exec(webpackCLIBin); - - // Note: these sizes are without Build Optimizer or any advanced optimizations in the CLI. - await expectFileSizeToBeUnder('dist/app.main.js', 656 * 1024); - await expectFileSizeToBeUnder('dist/501.app.main.js', 1 * 1024); - await expectFileSizeToBeUnder('dist/888.app.main.js', 2 * 1024); - await expectFileSizeToBeUnder('dist/972.app.main.js', 2 * 1024); - - - // test resource urls without ./ - await replaceInFile('app/app.component.ts', './app.component.html', 'app.component.html'); - await replaceInFile('app/app.component.ts', './app.component.scss', 'app.component.scss'); - - // test the inclusion of metadata - // This build also test resource URLs without ./ - await exec(webpackCLIBin, '--mode=development'); - await expectFileToMatch('dist/app.main.js', 'AppModule'); - - skipCleaning(); -} diff --git a/tests/legacy-cli/e2e/tests/schematics_cli/basic.ts b/tests/legacy-cli/e2e/tests/schematics_cli/basic.ts deleted file mode 100644 index fd2cf368dc50..000000000000 --- a/tests/legacy-cli/e2e/tests/schematics_cli/basic.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as path from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { exec, execAndWaitForOutputToMatch, silentNpm } from '../../utils/process'; -import { rimraf } from '../../utils/fs'; - -export default async function () { - // setup - const argv = getGlobalVariable('argv'); - if (argv.noglobal) { - return; - } - - await silentNpm( - 'install', - '-g', - '@angular-devkit/schematics-cli', - ); - await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); - - const startCwd = process.cwd(); - const schematicPath = path.join(startCwd, 'test-schematic'); - - try { - // create blank schematic - await exec('schematics', 'schematic', '--name', 'test-schematic'); - - process.chdir(path.join(startCwd, 'test-schematic')); - await execAndWaitForOutputToMatch( - 'schematics', - ['.:', '--list-schematics'], - /my-full-schematic/, - ); - - } finally { - // restore path - process.chdir(startCwd); - await rimraf(schematicPath); - } -} diff --git a/tests/legacy-cli/e2e/tests/schematics_cli/blank-test.ts b/tests/legacy-cli/e2e/tests/schematics_cli/blank-test.ts deleted file mode 100644 index badc3a37cdf0..000000000000 --- a/tests/legacy-cli/e2e/tests/schematics_cli/blank-test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { exec, silentNpm } from '../../utils/process'; -import { rimraf } from '../../utils/fs'; - -export default async function () { - // setup - const argv = getGlobalVariable('argv'); - if (argv.noglobal) { - return; - } - - await silentNpm( - 'install', - '-g', - '@angular-devkit/schematics-cli', - ); - await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); - - const startCwd = process.cwd(); - const schematicPath = path.join(startCwd, 'test-schematic'); - - try { - // create schematic - await exec('schematics', 'blank', '--name', 'test-schematic'); - - process.chdir(schematicPath); - - await silentNpm('install'); - await silentNpm('test'); - } finally { - // restore path - process.chdir(startCwd); - await rimraf(schematicPath); - } -} diff --git a/tests/legacy-cli/e2e/tests/schematics_cli/schematic-test.ts b/tests/legacy-cli/e2e/tests/schematics_cli/schematic-test.ts deleted file mode 100644 index 4c4993b4b0c3..000000000000 --- a/tests/legacy-cli/e2e/tests/schematics_cli/schematic-test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { getGlobalVariable } from '../../utils/env'; -import { exec, silentNpm } from '../../utils/process'; -import { rimraf } from '../../utils/fs'; - -export default async function () { - // setup - const argv = getGlobalVariable('argv'); - if (argv.noglobal) { - return; - } - - await silentNpm( - 'install', - '-g', - '@angular-devkit/schematics-cli', - ); - await exec(process.platform.startsWith('win') ? 'where' : 'which', 'schematics'); - - const startCwd = process.cwd(); - const schematicPath = path.join(startCwd, 'test-schematic'); - - try { - // create schematic - await exec('schematics', 'schematic', '--name', 'test-schematic'); - - process.chdir(schematicPath); - - await silentNpm('install'); - await silentNpm('test'); - } finally { - // restore path - process.chdir(startCwd); - await rimraf(schematicPath); - } -} diff --git a/tests/legacy-cli/e2e/tests/test/test-environment.ts b/tests/legacy-cli/e2e/tests/test/test-environment.ts deleted file mode 100644 index 09152caee409..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-environment.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ng } from '../../utils/process'; -import { writeFile } from '../../utils/fs'; -import { updateJsonFile } from '../../utils/project'; - -export default function () { - // Tests run in 'dev' environment by default. - return writeFile('src/app/environment.spec.ts', ` - import { environment } from '../environments/environment'; - - describe('Test environment', () => { - it('should have production disabled', () => { - expect(environment.production).toBe(false); - }); - }); - `) - .then(() => ng('test', '--watch=false')) - .then(() => updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.test.configurations = { - production: { - fileReplacements: [ - { - src: 'src/environments/environment.ts', - replaceWith: 'src/environments/environment.prod.ts', - } - ], - } - }; - })) - - // Tests can run in different environment. - .then(() => writeFile('src/app/environment.spec.ts', ` - import { environment } from '../environments/environment'; - - describe('Test environment', () => { - it('should have production enabled', () => { - expect(environment.production).toBe(true); - }); - }); - `)) - .then(() => ng('test', '--prod', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/test/test-fail-single-run.ts b/tests/legacy-cli/e2e/tests/test/test-fail-single-run.ts deleted file mode 100644 index 62ac51d0199a..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-fail-single-run.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ng } from '../../utils/process'; -import { writeFile } from '../../utils/fs'; -import { expectToFail } from '../../utils/utils'; - - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - // Fails on single run with broken compilation. - return writeFile('src/app.component.spec.ts', '

definitely not typescript

') - .then(() => expectToFail(() => ng('test', '--watch=false'))); -} diff --git a/tests/legacy-cli/e2e/tests/test/test-fail-watch.ts b/tests/legacy-cli/e2e/tests/test/test-fail-watch.ts deleted file mode 100644 index 69b28fabaeea..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-fail-watch.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - killAllProcesses, - waitForAnyProcessOutputToMatch, - execAndWaitForOutputToMatch, -} from '../../utils/process'; -import { expectToFail } from '../../utils/utils'; -import { readFile, writeFile } from '../../utils/fs'; - - -// Karma is only really finished with a run when it shows a non-zero total time in the first slot. -const karmaGoodRegEx = /Executed 3 of 3 SUCCESS \(\d+\.\d+ secs/; - -export default function () { - // TODO(architect): This test is behaving oddly both here and in devkit/build-angular. - // It seems to be because of file watchers. - return; - - let originalSpec: string; - return execAndWaitForOutputToMatch('ng', ['test'], karmaGoodRegEx) - .then(() => readFile('src/app/app.component.spec.ts')) - .then((data) => originalSpec = data) - // Trigger a failed rebuild, which shouldn't run tests again. - .then(() => writeFile('src/app/app.component.spec.ts', '

definitely not typescript

')) - .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(karmaGoodRegEx, 10000))) - // Restore working spec. - .then(() => writeFile('src/app/app.component.spec.ts', originalSpec)) - .then(() => waitForAnyProcessOutputToMatch(karmaGoodRegEx, 20000)) - .then(() => killAllProcesses(), (err: any) => { - killAllProcesses(); - throw err; - }); -} diff --git a/tests/legacy-cli/e2e/tests/test/test-scripts.ts b/tests/legacy-cli/e2e/tests/test/test-scripts.ts deleted file mode 100644 index 4114447796ae..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-scripts.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { writeMultipleFiles } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; -import { stripIndent } from 'common-tags'; - - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => ng('test', '--watch=false')) - // prepare global scripts test files - .then(() => writeMultipleFiles({ - 'src/string-script.js': `stringScriptGlobal = 'string-scripts.js';`, - 'src/input-script.js': `inputScriptGlobal = 'input-scripts.js';`, - 'src/typings.d.ts': stripIndent` - declare var stringScriptGlobal: any; - declare var inputScriptGlobal: any; - `, - 'src/app/app.component.ts': stripIndent` - import { Component } from '@angular/core'; - - @Component({ selector: 'app-root', template: '' }) - export class AppComponent { - stringScriptGlobalProp = stringScriptGlobal; - inputScriptGlobalProp = inputScriptGlobal; - } - `, - 'src/app/app.component.spec.ts': stripIndent` - import { TestBed } from '@angular/core/testing'; - import { AppComponent } from './app.component'; - - describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ AppComponent ] - }).compileComponents(); - }); - - it('should have access to string-script.js', () => { - let app = TestBed.createComponent(AppComponent).debugElement.componentInstance; - expect(app.stringScriptGlobalProp).toEqual('string-scripts.js'); - }); - - it('should have access to input-script.js', () => { - let app = TestBed.createComponent(AppComponent).debugElement.componentInstance; - expect(app.inputScriptGlobalProp).toEqual('input-scripts.js'); - }); - }); - - describe('Spec', () => { - it('should have access to string-script.js', () => { - expect(stringScriptGlobal).toBe('string-scripts.js'); - }); - - it('should have access to input-script.js', () => { - expect(inputScriptGlobal).toBe('input-scripts.js'); - }); - }); - ` - })) - // should fail because the global scripts were not added to scripts array - .then(() => expectToFail(() => ng('test', '--watch=false'))) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.test.options.scripts = [ - { input: 'src/string-script.js' }, - { input: 'src/input-script.js' }, - ]; - })) - // should pass now - .then(() => ng('test', '--watch=false')); -} - diff --git a/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts b/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts deleted file mode 100644 index 7bf102e0820a..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-sourcemap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { writeFile } from '../../utils/fs'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function () { - await writeFile('src/app/app.component.spec.ts', ` - it('show fail', () => { - expect(undefined).toBeTruthy(); - }); - `); - - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.test.options.sourceMap = { - scripts: true, - }; - }); - - // when sourcemaps are 'on' the stacktrace will point to the spec.ts file. - try { - await ng('test', '--watch', 'false'); - throw new Error('ng test should have failed.'); - } catch (error) { - if (!error.message.includes('app.component.spec.ts')) { - throw error; - }; - } - - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.test.options.sourceMap = true; - }); - - // when sourcemaps are 'on' the stacktrace will point to the spec.ts file. - try { - await ng('test', '--watch', 'false'); - throw new Error('ng test should have failed.'); - } catch (error) { - if (!error.message.includes('app.component.spec.ts')) { - throw error; - }; - } - - await updateJsonFile('angular.json', configJson => { - const appArchitect = configJson.projects['test-project'].architect; - appArchitect.test.options.sourceMap = false; - }); - - // when sourcemaps are 'off' the stacktrace won't point to the spec.ts file. - try { - await ng('test', '--watch', 'false'); - throw new Error('ng test should have failed.'); - } catch (error) { - if (!error.message.includes('main.js')) { - throw error; - }; - } -} diff --git a/tests/legacy-cli/e2e/tests/test/test-target.ts b/tests/legacy-cli/e2e/tests/test/test-target.ts deleted file mode 100644 index ba1afd13d1c2..000000000000 --- a/tests/legacy-cli/e2e/tests/test/test-target.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default function () { - // TODO(architect): This is giving odd errors in devkit/build-angular. - // TypeError: Assignment to constant variable. - return; - - return updateJsonFile('tsconfig.json', configJson => { - const compilerOptions = configJson['compilerOptions']; - compilerOptions['target'] = 'es2015'; - }) - .then(() => updateJsonFile('src/tsconfig.spec.json', configJson => { - const compilerOptions = configJson['compilerOptions']; - compilerOptions['target'] = 'es2015'; - })) - .then(() => ng('test', '--watch=false')); -} diff --git a/tests/legacy-cli/e2e/tests/third-party/bootstrap.ts b/tests/legacy-cli/e2e/tests/third-party/bootstrap.ts deleted file mode 100644 index 04e2a51b0213..000000000000 --- a/tests/legacy-cli/e2e/tests/third-party/bootstrap.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { installPackage } from '../../utils/packages'; -import {ng} from '../../utils/process'; -import {updateJsonFile} from '../../utils/project'; -import {expectFileToMatch} from '../../utils/fs'; -import {oneLineTrim} from 'common-tags'; - - -export default function() { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => installPackage('bootstrap@4.0.0-beta.3')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'node_modules/bootstrap/dist/css/bootstrap.css' }, - ]; - appArchitect.build.options.scripts = [ - { input: 'node_modules/bootstrap/dist/js/bootstrap.js' }, - ]; - })) - .then(() => ng('build', '--extract-css', '--configuration=development')) - .then(() => expectFileToMatch('dist/test-project/scripts.js', '* Bootstrap')) - .then(() => expectFileToMatch('dist/test-project/styles.css', '* Bootstrap')) - .then(() => expectFileToMatch('dist/test-project/index.html', oneLineTrim` - - `)) - .then(() => ng( - 'build', - '--configuration=development', - '--optimization', - '--extract-css', - '--output-hashing=none', - '--vendor-chunk=false', - )) - .then(() => expectFileToMatch('dist/test-project/scripts.js', 'jQuery')) - .then(() => expectFileToMatch('dist/test-project/styles.css', '* Bootstrap')) - .then(() => expectFileToMatch('dist/test-project/index.html', oneLineTrim` - - `)); -} diff --git a/tests/legacy-cli/e2e/tests/third-party/material-icons.ts b/tests/legacy-cli/e2e/tests/third-party/material-icons.ts deleted file mode 100644 index b8fc94763d25..000000000000 --- a/tests/legacy-cli/e2e/tests/third-party/material-icons.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expectFileToMatch } from '../../utils/fs'; -import { installPackage } from '../../utils/packages'; -import { ng } from '../../utils/process'; -import { updateJsonFile } from '../../utils/project'; - -export default async function() { - // Install material design icons - await installPackage('material-design-icons@3.0.1'); - - // Add icon stylesheet to application - await updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].architect; - appArchitect.build.options.styles = [ - { input: 'node_modules/material-design-icons/iconfont/material-icons.css' }, - ]; - }); - - // Build dev application - await ng('build', '--extract-css', '--configuration=development'); - - // Ensure icons are included - await expectFileToMatch('dist/test-project/styles.css', 'Material Icons'); - - // Build prod application - await ng('build', '--extract-css', '--output-hashing=none'); - - // Ensure icons are included - await expectFileToMatch('dist/test-project/styles.css', 'Material Icons'); -} diff --git a/tests/legacy-cli/e2e/tests/update/update-8.ts b/tests/legacy-cli/e2e/tests/update/update-8.ts deleted file mode 100644 index 872c4da269fd..000000000000 --- a/tests/legacy-cli/e2e/tests/update/update-8.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createProjectFromAsset } from '../../utils/assets'; -import { expectFileMatchToExist } from '../../utils/fs'; -import { installPackage, installWorkspacePackages, setRegistry } from '../../utils/packages'; -import { ng, noSilentNg } from '../../utils/process'; -import { isPrereleaseCli, useCIChrome, useCIDefaults } from '../../utils/project'; - -export default async function () { - // We need to use the public registry because in the local NPM server we don't have - // older versions @angular/cli packages which would cause `npm install` during `ng update` to fail. - try { - await createProjectFromAsset('8.0-project', true, true); - - await setRegistry(false); - await installWorkspacePackages(); - - // Update Angular to 9 - await installPackage('@angular/cli@8'); - const { stdout } = await ng('update', '@angular/cli@9.x', '@angular/core@9.x'); - if (!stdout.includes("Executing migrations of package '@angular/cli'")) { - throw new Error('Update did not execute migrations. OUTPUT: \n' + stdout); - } - - // Update Angular to 10 - await ng('update', '@angular/cli@10', '@angular/core@10'); - - // Update Angular to 11 - await ng('update', '@angular/cli@11', '@angular/core@11'); - } finally { - await setRegistry(true); - } - - // Update Angular current build - const extraUpdateArgs = isPrereleaseCli() ? ['--next', '--force'] : []; - // For the latest/next release we purposely don't add `@angular/core`. - // This is due to our bumping strategy, which causes a period were `@angular/cli@latest` (v12.0.0) `@angular/core@latest` (v11.2.x) - // are of different major/minor version on the local NPM server. This causes `ng update` to fail. - // NB: `ng update @angula/cli` will still cause `@angular/core` packages to be updated. - await ng('update', '@angular/cli', ...extraUpdateArgs); - - // Setup testing to use CI Chrome. - await useCIChrome('./'); - await useCIChrome('./e2e/'); - await useCIDefaults('eight-project'); - - // Run CLI commands. - await ng('generate', 'component', 'my-comp'); - await ng('test', '--watch=false'); - await ng('lint'); - await ng('e2e'); - await ng('e2e', '--prod'); - - // Verify project now creates bundles for differential loading. - await noSilentNg('build', '--prod'); - await expectFileMatchToExist('dist/eight-project/', /main-es5\.[0-9a-f]{20}\.js/); - await expectFileMatchToExist('dist/eight-project/', /main-es2015\.[0-9a-f]{20}\.js/); -} diff --git a/tests/legacy-cli/e2e/utils/assets.ts b/tests/legacy-cli/e2e/utils/assets.ts deleted file mode 100644 index e2d3f2f0a969..000000000000 --- a/tests/legacy-cli/e2e/utils/assets.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { join } from 'path'; -import * as glob from 'glob'; -import { getGlobalVariable } from './env'; -import { relative, resolve } from 'path'; -import { copyFile, writeFile } from './fs'; -import { installWorkspacePackages } from './packages'; -import { useBuiltPackages } from './project'; - -export function assetDir(assetName: string) { - return join(__dirname, '../assets', assetName); -} - -export function copyProjectAsset(assetName: string, to?: string) { - const tempRoot = join(getGlobalVariable('tmp-root'), 'test-project'); - const sourcePath = assetDir(assetName); - const targetPath = join(tempRoot, to || assetName); - - return copyFile(sourcePath, targetPath); -} - -export function copyAssets(assetName: string, to?: string) { - const seed = +Date.now(); - const tempRoot = join(getGlobalVariable('tmp-root'), 'assets', assetName + '-' + seed); - const root = assetDir(assetName); - - return Promise.resolve() - .then(() => { - const allFiles = glob.sync(join(root, '**/*'), { dot: true, nodir: true }); - - return allFiles.reduce((promise, filePath) => { - const relPath = relative(root, filePath); - const toPath = - to !== undefined - ? resolve(getGlobalVariable('tmp-root'), 'test-project', to, relPath) - : join(tempRoot, relPath); - - return promise.then(() => copyFile(filePath, toPath)); - }, Promise.resolve()); - }) - .then(() => tempRoot); -} - -export async function createProjectFromAsset( - assetName: string, - useNpmPackages = false, - skipInstall = false, -) { - const dir = await copyAssets(assetName); - process.chdir(dir); - if (!useNpmPackages) { - await useBuiltPackages(); - if (!getGlobalVariable('ci')) { - const testRegistry = getGlobalVariable('package-registry'); - await writeFile('.npmrc', `registry=${testRegistry}`); - } - } - - if (!skipInstall) { - await installWorkspacePackages(); - } - - return dir; -} diff --git a/tests/legacy-cli/e2e/utils/env.ts b/tests/legacy-cli/e2e/utils/env.ts deleted file mode 100644 index dd80596f0698..000000000000 --- a/tests/legacy-cli/e2e/utils/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -const global: {[name: string]: any} = Object.create(null); - - -export function setGlobalVariable(name: string, value: any) { - global[name] = value; -} - -export function getGlobalVariable(name: string): any { - if (!(name in global)) { - throw new Error(`Trying to access variable "${name}" but it's not defined.`); - } - return global[name]; -} diff --git a/tests/legacy-cli/e2e/utils/fs.ts b/tests/legacy-cli/e2e/utils/fs.ts deleted file mode 100644 index 3123ec6f5a5d..000000000000 --- a/tests/legacy-cli/e2e/utils/fs.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { promises as fs, constants } from 'fs'; -import { dirname, join } from 'path'; -import { stripIndents } from 'common-tags'; - -export function readFile(fileName: string): Promise { - return fs.readFile(fileName, 'utf-8'); -} - -export function writeFile(fileName: string, content: string, options?: any): Promise { - return fs.writeFile(fileName, content, options); -} - -export function deleteFile(path: string): Promise { - return fs.unlink(path); -} - -export function rimraf(path: string): Promise { - return fs.rmdir(path, { recursive: true, maxRetries: 3 }); -} - -export function moveFile(from: string, to: string): Promise { - return fs.rename(from, to); -} - -export function symlinkFile(from: string, to: string, type?: string): Promise { - return fs.symlink(from, to, type); -} - -export function createDir(path: string): Promise { - return fs.mkdir(path, { recursive: true }); -} - -export async function copyFile(from: string, to: string): Promise { - await createDir(dirname(to)); - - return fs.copyFile(from, to, constants.COPYFILE_FICLONE); -} - -export async function moveDirectory(from: string, to: string): Promise { - await rimraf(to); - await createDir(to); - - for (const entry of await fs.readdir(from)) { - const fromEntry = join(from, entry); - const toEntry = join(to, entry); - if ((await fs.stat(fromEntry)).isFile()) { - await copyFile(fromEntry, toEntry); - } else { - await moveDirectory(fromEntry, toEntry); - } - } -} - -export function writeMultipleFiles(fs: { [path: string]: string }) { - return Promise.all(Object.keys(fs).map((fileName) => writeFile(fileName, fs[fileName]))); -} - -export function replaceInFile(filePath: string, match: RegExp | string, replacement: string) { - return readFile(filePath).then((content: string) => - writeFile(filePath, content.replace(match, replacement)), - ); -} - -export function appendToFile(filePath: string, text: string, options?: any) { - return readFile(filePath).then((content: string) => - writeFile(filePath, content.concat(text), options), - ); -} - -export function prependToFile(filePath: string, text: string, options?: any) { - return readFile(filePath).then((content: string) => - writeFile(filePath, text.concat(content), options), - ); -} - -export async function expectFileMatchToExist(dir: string, regex: RegExp): Promise { - const files = await fs.readdir(dir); - const fileName = files.find((name) => regex.test(name)); - - if (!fileName) { - throw new Error(`File ${regex} was expected to exist but not found...`); - } - - return fileName; -} - -export async function expectFileNotToExist(fileName: string): Promise { - try { - await fs.access(fileName, constants.F_OK); - } catch { - return; - } - - throw new Error(`File ${fileName} was expected not to exist but found...`); -} - -export async function expectFileToExist(fileName: string): Promise { - try { - await fs.access(fileName, constants.F_OK); - } catch { - throw new Error(`File ${fileName} was expected to exist but not found...`); - } -} - -export function expectFileToMatch(fileName: string, regEx: RegExp | string) { - return readFile(fileName).then((content) => { - if (typeof regEx == 'string') { - if (content.indexOf(regEx) == -1) { - throw new Error(stripIndents`File "${fileName}" did not contain "${regEx}"... - Content: - ${content} - ------ - `); - } - } else { - if (!content.match(regEx)) { - throw new Error(stripIndents`File "${fileName}" did not contain "${regEx}"... - Content: - ${content} - ------ - `); - } - } - }); -} - -export async function getFileSize(fileName: string) { - const stats = await fs.stat(fileName); - - return stats.size; -} - -export async function expectFileSizeToBeUnder(fileName: string, sizeInBytes: number) { - const fileSize = await getFileSize(fileName); - - if (fileSize > sizeInBytes) { - throw new Error( - `File "${fileName}" exceeded file size of "${sizeInBytes}". Size is ${fileSize}.`, - ); - } -} diff --git a/tests/legacy-cli/e2e/utils/git.ts b/tests/legacy-cli/e2e/utils/git.ts deleted file mode 100644 index 7da09d308c01..000000000000 --- a/tests/legacy-cli/e2e/utils/git.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {git, silentGit} from './process'; - - -export function gitClean() { - console.log(' Cleaning git...'); - return silentGit('clean', '-df') - .then(() => silentGit('reset', '--hard')) - .then(() => { - // Checkout missing files - return silentGit('status', '--porcelain') - .then(({ stdout }) => stdout - .split(/[\n\r]+/g) - .filter(line => line.match(/^ D/)) - .map(line => line.replace(/^\s*\S+\s+/, ''))) - .then(files => silentGit('checkout', ...files)); - }) - .then(() => expectGitToBeClean()); -} - -export function expectGitToBeClean() { - return silentGit('status', '--porcelain') - .then(({ stdout }) => { - if (stdout != '') { - throw new Error('Git repo is not clean...\n' + stdout); - } - }); -} - -export function gitCommit(message: string) { - return git('add', '-A') - .then(() => silentGit('status', '--porcelain')) - .then(({ stdout }) => { - if (stdout != '') { - return git('commit', '-am', message); - } - }); -} diff --git a/tests/legacy-cli/e2e/utils/http.ts b/tests/legacy-cli/e2e/utils/http.ts deleted file mode 100644 index fa0fbc32c3c5..000000000000 --- a/tests/legacy-cli/e2e/utils/http.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {IncomingMessage} from 'http'; -import * as _request from 'request'; - - -export function request(url: string): Promise { - return new Promise((resolve, reject) => { - let options = { - url: url, - headers: { 'Accept': 'text/html' }, - agentOptions: { rejectUnauthorized: false } - }; - _request(options, (error: any, response: IncomingMessage, body: string) => { - if (error) { - reject(error); - } else if (response.statusCode >= 400) { - reject(new Error(`Requesting "${url}" returned status code ${response.statusCode}.`)); - } else { - resolve(body); - } - }); - }); -} diff --git a/tests/legacy-cli/e2e/utils/packages.ts b/tests/legacy-cli/e2e/utils/packages.ts deleted file mode 100644 index a8416c9ae24c..000000000000 --- a/tests/legacy-cli/e2e/utils/packages.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getGlobalVariable } from './env'; -import { writeFile } from './fs'; -import { ProcessOutput, npm, silentNpm, silentYarn } from './process'; - -export function getActivePackageManager(): 'npm' | 'yarn' { - const value = getGlobalVariable('package-manager'); - if (value && value !== 'npm' && value !== 'yarn') { - throw new Error('Invalid package manager value: ' + value); - } - - return value || 'npm'; -} - -export async function installWorkspacePackages(): Promise { - switch (getActivePackageManager()) { - case 'npm': - await silentNpm('install'); - break; - case 'yarn': - await silentYarn(); - break; - } -} - -export async function installPackage(specifier: string, registry?: string): Promise { - const registryOption = registry ? [`--registry=${registry}`] : []; - switch (getActivePackageManager()) { - case 'npm': - return silentNpm('install', specifier, ...registryOption); - case 'yarn': - return silentYarn('add', specifier, ...registryOption); - } -} - -export async function uninstallPackage(name: string): Promise { - switch (getActivePackageManager()) { - case 'npm': - return silentNpm('uninstall', name); - case 'yarn': - return silentYarn('remove', name); - } -} - -export async function setRegistry(useTestRegistry: boolean): Promise { - const url = useTestRegistry - ? getGlobalVariable('package-registry') - : 'https://registry.npmjs.org'; - - const isCI = getGlobalVariable('ci'); - - // Ensure local test registry is used when outside a project - if (isCI) { - // Safe to set a user configuration on CI - await npm('config', 'set', 'registry', url); - } else { - // Yarn does not use the environment variable so an .npmrc file is also required - await writeFile('.npmrc', `registry=${url}`); - process.env['NPM_CONFIG_REGISTRY'] = url; - } -} diff --git a/tests/legacy-cli/e2e/utils/process.ts b/tests/legacy-cli/e2e/utils/process.ts deleted file mode 100644 index add60058cb17..000000000000 --- a/tests/legacy-cli/e2e/utils/process.ts +++ /dev/null @@ -1,233 +0,0 @@ -import * as ansiColors from 'ansi-colors'; -import { SpawnOptions } from "child_process"; -import * as child_process from 'child_process'; -import { concat, defer, EMPTY, from} from 'rxjs'; -import {repeat, takeLast} from 'rxjs/operators'; -import {getGlobalVariable} from './env'; -import {catchError} from 'rxjs/operators'; -const treeKill = require('tree-kill'); - - -interface ExecOptions { - silent?: boolean; - waitForMatch?: RegExp; - env?: { [varname: string]: string }; -} - - -let _processes: child_process.ChildProcess[] = []; - -export type ProcessOutput = { - stdout: string; - stderr: string; -}; - - -function _exec(options: ExecOptions, cmd: string, args: string[]): Promise { - // Create a separate instance to prevent unintended global changes to the color configuration - // Create function is not defined in the typings. See: https://github.com/doowb/ansi-colors/pull/44 - const colors = (ansiColors as typeof ansiColors & { create: () => typeof ansiColors }).create(); - - let stdout = ''; - let stderr = ''; - const cwd = process.cwd(); - const env = options.env; - console.log( - `==========================================================================================` - ); - - args = args.filter(x => x !== undefined); - const flags = [ - options.silent && 'silent', - options.waitForMatch && `matching(${options.waitForMatch})` - ] - .filter(x => !!x) // Remove false and undefined. - .join(', ') - .replace(/^(.+)$/, ' [$1]'); // Proper formatting. - - console.log(colors.blue(`Running \`${cmd} ${args.map(x => `"${x}"`).join(' ')}\`${flags}...`)); - console.log(colors.blue(`CWD: ${cwd}`)); - console.log(colors.blue(`ENV: ${JSON.stringify(env)}`)); - const spawnOptions: SpawnOptions = { - cwd, - ...env ? { env } : {}, - }; - - if (process.platform.startsWith('win')) { - args.unshift('/c', cmd); - cmd = 'cmd.exe'; - spawnOptions['stdio'] = 'pipe'; - } - - const childProcess = child_process.spawn(cmd, args, spawnOptions); - childProcess.stdout.on('data', (data: Buffer) => { - stdout += data.toString('utf-8'); - if (options.silent) { - return; - } - data.toString('utf-8') - .split(/[\n\r]+/) - .filter(line => line !== '') - .forEach(line => console.log(' ' + line)); - }); - childProcess.stderr.on('data', (data: Buffer) => { - stderr += data.toString('utf-8'); - if (options.silent) { - return; - } - data.toString('utf-8') - .split(/[\n\r]+/) - .filter(line => line !== '') - .forEach(line => console.error(colors.yellow(' ' + line))); - }); - - _processes.push(childProcess); - - // Create the error here so the stack shows who called this function. - const err = new Error(`Running "${cmd} ${args.join(' ')}" returned error code `); - return new Promise((resolve, reject) => { - childProcess.on('exit', (error: any) => { - _processes = _processes.filter(p => p !== childProcess); - - if (!error) { - resolve({ stdout, stderr }); - } else { - err.message += `${error}...\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}\n`; - reject(err); - } - }); - - if (options.waitForMatch) { - const match = options.waitForMatch; - childProcess.stdout.on('data', (data: Buffer) => { - if (data.toString().match(match)) { - resolve({ stdout, stderr }); - } - }); - childProcess.stderr.on('data', (data: Buffer) => { - if (data.toString().match(match)) { - resolve({ stdout, stderr }); - } - }); - } - }); -} - -export function waitForAnyProcessOutputToMatch(match: RegExp, - timeout = 30000): Promise { - // Race between _all_ processes, and the timeout. First one to resolve/reject wins. - const timeoutPromise: Promise = new Promise((_resolve, reject) => { - // Wait for 30 seconds and timeout. - setTimeout(() => { - reject(new Error(`Waiting for ${match} timed out (timeout: ${timeout}msec)...`)); - }, timeout); - }); - - const matchPromises: Promise[] = _processes.map( - childProcess => new Promise(resolve => { - let stdout = ''; - let stderr = ''; - childProcess.stdout.on('data', (data: Buffer) => { - stdout += data.toString(); - if (data.toString().match(match)) { - resolve({ stdout, stderr }); - } - }); - childProcess.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); - if (data.toString().match(match)) { - resolve({ stdout, stderr }); - } - }); - })); - - return Promise.race(matchPromises.concat([timeoutPromise])); -} - -export function killAllProcesses(signal = 'SIGTERM') { - _processes.forEach(process => treeKill(process.pid, signal)); - _processes = []; -} - -export function exec(cmd: string, ...args: string[]) { - return _exec({}, cmd, args); -} - -export function silentExec(cmd: string, ...args: string[]) { - return _exec({ silent: true }, cmd, args); -} - -export function execWithEnv(cmd: string, args: string[], env: { [varname: string]: string }) { - return _exec({ env }, cmd, args); -} - -export function execAndWaitForOutputToMatch(cmd: string, args: string[], match: RegExp) { - if (cmd === 'ng' && args[0] === 'serve') { - // Accept matches up to 20 times after the initial match. - // Useful because the Webpack watcher can rebuild a few times due to files changes that - // happened just before the build (e.g. `git clean`). - // This seems to be due to host file system differences, see - // https://nodejs.org/docs/latest/api/fs.html#fs_caveats - return concat( - from( - _exec({ waitForMatch: match }, cmd, args) - ), - defer(() => waitForAnyProcessOutputToMatch(match, 2500)).pipe( - repeat(20), - catchError(() => EMPTY), - ), - ).pipe( - takeLast(1), - ).toPromise(); - } else { - return _exec({ waitForMatch: match }, cmd, args); - } -} - -export function ng(...args: string[]) { - const argv = getGlobalVariable('argv'); - const maybeSilentNg = argv['nosilent'] ? noSilentNg : silentNg; - if (['build', 'serve', 'test', 'e2e', 'extract-i18n'].indexOf(args[0]) != -1) { - if (args[0] == 'e2e') { - // Wait 1 second before running any end-to-end test. - return new Promise(resolve => setTimeout(resolve, 1000)) - .then(() => maybeSilentNg(...args)); - } - - return maybeSilentNg(...args); - } else { - return noSilentNg(...args); - } -} - -export function noSilentNg(...args: string[]) { - return _exec({}, 'ng', args); -} - -export function silentNg(...args: string[]) { - return _exec({silent: true}, 'ng', args); -} - -export function silentNpm(...args: string[]) { - return _exec({silent: true}, 'npm', args); -} - -export function silentYarn(...args: string[]) { - return _exec({silent: true}, 'yarn', args); -} - -export function npm(...args: string[]) { - return _exec({}, 'npm', args); -} - -export function node(...args: string[]) { - return _exec({}, 'node', args); -} - -export function git(...args: string[]) { - return _exec({}, 'git', args); -} - -export function silentGit(...args: string[]) { - return _exec({silent: true}, 'git', args); -} diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts deleted file mode 100644 index df5317e0cb56..000000000000 --- a/tests/legacy-cli/e2e/utils/project.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { prerelease } from 'semver'; -import { packages } from '../../../../lib/packages'; -import { getGlobalVariable } from './env'; -import { prependToFile, readFile, replaceInFile, writeFile } from './fs'; -import { gitCommit } from './git'; -import { installWorkspacePackages } from './packages'; -import { execAndWaitForOutputToMatch, git, ng } from './process'; - -export function updateJsonFile(filePath: string, fn: (json: any) => any | void) { - return readFile(filePath).then((tsConfigJson) => { - // Remove single and multiline comments - const tsConfig = JSON.parse(tsConfigJson.replace(/\/\*\s(.|\n|\r)*\s\*\/|\/\/.*/g, '')); - const result = fn(tsConfig) || tsConfig; - - return writeFile(filePath, JSON.stringify(result, null, 2)); - }); -} - -export function updateTsConfig(fn: (json: any) => any | void) { - return updateJsonFile('tsconfig.json', fn); -} - -export function ngServe(...args: string[]) { - return execAndWaitForOutputToMatch('ng', ['serve', ...args], / Compiled successfully./); -} - -export async function prepareProjectForE2e(name) { - const argv: string[] = getGlobalVariable('argv'); - - await git('config', 'user.email', 'angular-core+e2e@google.com'); - await git('config', 'user.name', 'Angular CLI E2e'); - await git('config', 'commit.gpgSign', 'false'); - - await ng('generate', '@schematics/angular:e2e', '--related-app-name', name); - - await useCIChrome('e2e'); - await useCIChrome(''); - - // legacy projects - await useCIChrome('src'); - - if (argv['ng-snapshots'] || argv['ng-tag']) { - await useSha(); - } - - console.log(`Project ${name} created... Installing npm.`); - await installWorkspacePackages(); - await useCIDefaults(name); - // Force sourcemaps to be from the root of the filesystem. - await updateJsonFile('tsconfig.json', (json) => { - json['compilerOptions']['sourceRoot'] = '/'; - }); - await gitCommit('prepare-project-for-e2e'); -} - -export function useBuiltPackages() { - return Promise.resolve().then(() => - updateJsonFile('package.json', (json) => { - if (!json['dependencies']) { - json['dependencies'] = {}; - } - if (!json['devDependencies']) { - json['devDependencies'] = {}; - } - - for (const packageName of Object.keys(packages)) { - if (json['dependencies'].hasOwnProperty(packageName)) { - json['dependencies'][packageName] = packages[packageName].tar; - } else if (json['devDependencies'].hasOwnProperty(packageName)) { - json['devDependencies'][packageName] = packages[packageName].tar; - } - } - }), - ); -} - -export function useSha() { - const argv = getGlobalVariable('argv'); - if (argv['ng-snapshots'] || argv['ng-tag']) { - // We need more than the sha here, version is also needed. Examples of latest tags: - // 7.0.0-beta.4+dd2a650 - // 6.1.6+4a8d56a - const label = argv['ng-tag'] ? argv['ng-tag'] : ''; - const ngSnapshotVersions = require('../ng-snapshot/package.json'); - return updateJsonFile('package.json', (json) => { - // Install over the project with snapshot builds. - function replaceDependencies(key: string) { - const missingSnapshots = []; - Object.keys(json[key] || {}) - .filter((name) => name.match(/^@angular\//)) - .forEach((name) => { - const pkgName = name.split(/\//)[1]; - if (pkgName == 'cli') { - return; - } - if (label) { - json[key][`@angular/${pkgName}`] = `github:angular/${pkgName}-builds${label}`; - } else { - const replacement = ngSnapshotVersions.dependencies[`@angular/${pkgName}`]; - if (!replacement) { - missingSnapshots.push(`missing @angular/${pkgName}`); - } - json[key][`@angular/${pkgName}`] = replacement; - } - }); - if (missingSnapshots.length > 0) { - throw new Error( - 'e2e test with --ng-snapshots requires all angular packages be ' + - 'listed in tests/legacy-cli/e2e/ng-snapshot/package.json.\nErrors:\n' + - missingSnapshots.join('\n '), - ); - } - } - try { - replaceDependencies('dependencies'); - replaceDependencies('devDependencies'); - } catch (e) { - return Promise.reject(e); - } - }); - } else { - return Promise.resolve(); - } -} - -export function useNgVersion(version: string) { - return updateJsonFile('package.json', (json) => { - // Install over the project with specific versions. - Object.keys(json['dependencies'] || {}) - .filter((name) => name.match(/^@angular\//)) - .forEach((name) => { - const pkgName = name.split(/\//)[1]; - if (pkgName == 'cli') { - return; - } - json['dependencies'][`@angular/${pkgName}`] = version; - }); - - Object.keys(json['devDependencies'] || {}) - .filter((name) => name.match(/^@angular\//)) - .forEach((name) => { - const pkgName = name.split(/\//)[1]; - if (pkgName == 'cli') { - return; - } - json['devDependencies'][`@angular/${pkgName}`] = version; - }); - // Set the correct peer dependencies for @angular/core and @angular/compiler-cli. - // This list should be kept up to date with each major release. - if (version.startsWith('^5')) { - json['devDependencies']['typescript'] = '>=2.4.2 <2.5'; - json['dependencies']['rxjs'] = '^5.5.0'; - json['dependencies']['zone.js'] = '~0.8.4'; - } else if (version.startsWith('^6')) { - json['devDependencies']['typescript'] = '>=2.7.2 <2.8'; - json['dependencies']['rxjs'] = '^6.0.0'; - json['dependencies']['zone.js'] = '~0.8.26'; - } else if (version.startsWith('^7')) { - json['devDependencies']['typescript'] = '>=3.1.1 <3.2'; - json['dependencies']['rxjs'] = '^6.0.0'; - json['dependencies']['zone.js'] = '~0.8.26'; - } - }); -} - -export function useCIDefaults(projectName = 'test-project') { - return updateJsonFile('angular.json', (workspaceJson) => { - // Disable progress reporting on CI to reduce spam. - const project = workspaceJson.projects[projectName]; - const appTargets = project.targets || project.architect; - appTargets.build.options.progress = false; - appTargets.test.options.progress = false; - // Disable auto-updating webdriver in e2e. - if (appTargets.e2e) { - appTargets.e2e.options.webdriverUpdate = false; - } - - // legacy project structure - const e2eProject = workspaceJson.projects[projectName + '-e2e']; - if (e2eProject) { - const e2eTargets = e2eProject.targets || e2eProject.architect; - e2eTargets.e2e.options.webdriverUpdate = false; - } - }); -} - -export async function useCIChrome(projectDir: string = ''): Promise { - const protractorConf = path.join(projectDir, 'protractor.conf.js'); - const karmaConf = path.join(projectDir, 'karma.conf.js'); - - const chromePath = require('puppeteer').executablePath(); - const protractorPath = require.resolve('protractor'); - const webdriverUpdatePath = require.resolve('webdriver-manager/selenium/update-config.json', { - paths: [protractorPath], - }); - const webdriverUpdate = JSON.parse(await readFile(webdriverUpdatePath)) as { - chrome: { last: string }; - }; - const chromeDriverPath = webdriverUpdate.chrome.last; - - // Use Puppeteer in protractor if a config is found on the project. - if (fs.existsSync(protractorConf)) { - await replaceInFile( - protractorConf, - `browserName: 'chrome'`, - `browserName: 'chrome', - chromeOptions: { - args: ['--headless'], - binary: String.raw\`${chromePath}\`, - }`, - ); - await replaceInFile( - protractorConf, - 'directConnect: true,', - `directConnect: true, chromeDriver: String.raw\`${chromeDriverPath}\`,`, - ); - } - - // Use Puppeteer in karma if a config is found on the project. - if (fs.existsSync(karmaConf)) { - await prependToFile(karmaConf, `process.env.CHROME_BIN = String.raw\`${chromePath}\`;`); - await replaceInFile(karmaConf, `browsers: ['Chrome']`, `browsers: ['ChromeHeadless']`); - } -} - -export function isPrereleaseCli(): boolean { - const pre = prerelease(packages['@angular/cli'].version); - - return pre && pre.length > 0; -} diff --git a/tests/legacy-cli/e2e/utils/utils.ts b/tests/legacy-cli/e2e/utils/utils.ts deleted file mode 100644 index 3f73ec59ba94..000000000000 --- a/tests/legacy-cli/e2e/utils/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ - -export function expectToFail(fn: () => Promise, errorMessage?: string): Promise { - return fn() - .then(() => { - const functionSource = fn.name || (fn).source || fn.toString(); - const errorDetails = errorMessage ? `\n\tDetails:\n\t${errorMessage}` : ''; - throw new Error( - `Function ${functionSource} was expected to fail, but succeeded.${errorDetails}`); - }, (err) => { return err; }); -} - -export function wait(msecs: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, msecs); - }); -} diff --git a/tests/legacy-cli/e2e/utils/version.ts b/tests/legacy-cli/e2e/utils/version.ts deleted file mode 100644 index 0be47e2216e3..000000000000 --- a/tests/legacy-cli/e2e/utils/version.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import * as semver from 'semver'; - - -export function readNgVersion(): string { - const packageJson: any = JSON.parse(fs.readFileSync('./node_modules/@angular/core/package.json', 'utf8')); - return packageJson['version']; -} - -export function ngVersionMatches(range: string): boolean { - return semver.satisfies(readNgVersion(), range); -} diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts deleted file mode 100644 index 182bc2edb714..000000000000 --- a/tests/legacy-cli/e2e_runner.ts +++ /dev/null @@ -1,263 +0,0 @@ -// This may seem awkward but we're using Logger in our e2e. At this point the unit tests -// have run already so it should be "safe", teehee. -import { logging } from '@angular-devkit/core'; -import { createConsoleLogger } from '@angular-devkit/core/node'; -import * as colors from 'ansi-colors'; -import { spawn } from 'child_process'; -import * as fs from 'fs'; -import * as glob from 'glob'; -import * as minimist from 'minimist'; -import * as os from 'os'; -import * as path from 'path'; -import { setGlobalVariable } from './e2e/utils/env'; -import { gitClean } from './e2e/utils/git'; - -Error.stackTraceLimit = Infinity; - -// tslint:disable:no-global-tslint-disable no-console - -/** - * Here's a short description of those flags: - * --debug If a test fails, block the thread so the temporary directory isn't deleted. - * --noproject Skip creating a project or using one. - * --nobuild Skip building the packages. Use with --noglobal and --reuse to quickly - * rerun tests. - * --noglobal Skip linking your local @angular/cli directory. Can save a few seconds. - * --nosilent Never silence ng commands. - * --ng-tag=TAG Use a specific tag for build snapshots. Similar to ng-snapshots but point to a - * tag instead of using the latest master. - * --ng-snapshots Install angular snapshot builds in the test project. - * --ve Use the View Engine compiler. - * --glob Run tests matching this glob pattern (relative to tests/e2e/). - * --ignore Ignore tests matching this glob pattern. - * --reuse=/path Use a path instead of create a new project. That project should have been - * created, and npm installed. Ideally you want a project created by a previous - * run of e2e. - * --nb-shards Total number of shards that this is part of. Default is 2 if --shard is - * passed in. - * --shard Index of this processes' shard. - * --devkit=path Path to the devkit to use. The devkit will be built prior to running. - * --tmpdir=path Override temporary directory to use for new projects. - * If unnamed flags are passed in, the list of tests will be filtered to include only those passed. - */ -const argv = minimist(process.argv.slice(2), { - boolean: ['debug', 'ng-snapshots', 'noglobal', 'nosilent', 'noproject', 'verbose'], - string: ['devkit', 'glob', 'ignore', 'reuse', 'ng-tag', 'tmpdir', 'ng-version'], -}); - -/** - * Set the error code of the process to 255. This is to ensure that if something forces node - * to exit without finishing properly, the error code will be 255. Right now that code is not used. - * - * - 1 When tests succeed we already call `process.exit(0)`, so this doesn't change any correct - * behaviour. - * - * One such case that would force node <= v6 to exit with code 0, is a Promise that doesn't resolve. - */ -process.exitCode = 255; - -const logger = createConsoleLogger(argv.verbose, process.stdout, process.stderr, { - info: s => s, - debug: s => s, - warn: s => colors.bold.yellow(s), - error: s => colors.bold.red(s), - fatal: s => colors.bold.red(s), -}); - -const logStack = [logger]; -function lastLogger() { - return logStack[logStack.length - 1]; -} - -const testGlob = argv.glob || 'tests/**/*.ts'; -let currentFileName = null; - -const e2eRoot = path.join(__dirname, 'e2e'); -const allSetups = glob - .sync(path.join(e2eRoot, 'setup/**/*.ts'), { nodir: true }) - .map(name => path.relative(e2eRoot, name)) - .sort(); -const allTests = glob - .sync(path.join(e2eRoot, testGlob), { nodir: true, ignore: argv.ignore }) - .map(name => path.relative(e2eRoot, name)) - // Replace windows slashes. - .map(name => name.replace(/\\/g, '/')) - .sort() - .filter(name => !name.endsWith('/setup.ts')); - -const shardId = 'shard' in argv ? argv['shard'] : null; -const nbShards = (shardId === null ? 1 : argv['nb-shards']) || 2; -const tests = allTests.filter(name => { - // Check for naming tests on command line. - if (argv._.length == 0) { - return true; - } - - return argv._.some(argName => { - return ( - path.join(process.cwd(), argName) == path.join(__dirname, 'e2e', name) || - argName == name || - argName == name.replace(/\.ts$/, '') - ); - }); -}); - -// Remove tests that are not part of this shard. -const shardedTests = tests.filter((name, i) => shardId === null || i % nbShards == shardId); -const testsToRun = allSetups.concat(shardedTests); - -if (shardedTests.length === 0) { - console.log(`No tests would be ran, aborting.`); - process.exit(1); -} - -console.log(testsToRun.join('\n')); -/** - * Load all the files from the e2e, filter and sort them and build a promise of their default - * export. - */ -if (testsToRun.length == allTests.length) { - console.log(`Running ${testsToRun.length} tests`); -} else { - console.log(`Running ${testsToRun.length} tests (${allTests.length + allSetups.length} total)`); -} - -setGlobalVariable('argv', argv); -setGlobalVariable('ci', process.env['CI']?.toLowerCase() === 'true' || process.env['CI'] === '1'); -setGlobalVariable('package-manager', argv.yarn ? 'yarn' : 'npm'); -setGlobalVariable('package-registry', 'http://localhost:4873'); - -// Setup local package registry -const registryPath = - fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'angular-cli-e2e-registry-')); -fs.copyFileSync( - path.join(__dirname, 'verdaccio.yaml'), - path.join(registryPath, 'verdaccio.yaml'), -); -const registryProcess = spawn( - 'node', - [require.resolve('verdaccio/bin/verdaccio'), '-c', './verdaccio.yaml'], - { cwd: registryPath, stdio: 'inherit' }, -); - -testsToRun - .reduce((previous, relativeName, testIndex) => { - // Make sure this is a windows compatible path. - let absoluteName = path.join(e2eRoot, relativeName); - if (/^win/.test(process.platform)) { - absoluteName = absoluteName.replace(/\\/g, path.posix.sep); - } - - return previous.then(() => { - currentFileName = relativeName.replace(/\.ts$/, ''); - const start = +new Date(); - - const module = require(absoluteName); - const fn: (skipClean?: () => void) => Promise | void = - typeof module == 'function' - ? module - : typeof module.default == 'function' - ? module.default - : () => { - throw new Error('Invalid test module.'); - }; - - let clean = true; - let previousDir = null; - - return Promise.resolve() - .then(() => printHeader(currentFileName, testIndex)) - .then(() => (previousDir = process.cwd())) - .then(() => logStack.push(lastLogger().createChild(currentFileName))) - .then(() => fn(() => (clean = false))) - .then( - () => logStack.pop(), - err => { - logStack.pop(); - throw err; - }, - ) - .then(() => console.log('----')) - .then(() => { - // If we're not in a setup, change the directory back to where it was before the test. - // This allows tests to chdir without worrying about keeping the original directory. - if (allSetups.indexOf(relativeName) == -1 && previousDir) { - process.chdir(previousDir); - } - }) - .then(() => { - // Only clean after a real test, not a setup step. Also skip cleaning if the test - // requested an exception. - if (allSetups.indexOf(relativeName) == -1 && clean) { - logStack.push(new logging.NullLogger()); - - return gitClean().then( - () => logStack.pop(), - err => { - logStack.pop(); - throw err; - }, - ); - } - }) - .then( - () => printFooter(currentFileName, start), - err => { - printFooter(currentFileName, start); - console.error(err); - throw err; - }, - ); - }); - }, Promise.resolve()) - .then( - () => { - if (registryProcess) { - registryProcess.kill(); - } - - console.log(colors.green('Done.')); - process.exit(0); - }, - err => { - console.log('\n'); - console.error(colors.red(`Test "${currentFileName}" failed...`)); - console.error(colors.red(err.message)); - console.error(colors.red(err.stack)); - - if (registryProcess) { - registryProcess.kill(); - } - - if (argv.debug) { - console.log(`Current Directory: ${process.cwd()}`); - console.log('Will loop forever while you debug... CTRL-C to quit.'); - - /* eslint-disable no-constant-condition */ - while (1) { - // That's right! - } - } - - process.exit(1); - }, - ); - -function printHeader(testName: string, testIndex: number) { - const text = `${testIndex + 1} of ${testsToRun.length}`; - const fullIndex = - (testIndex < allSetups.length - ? testIndex - : (testIndex - allSetups.length) * nbShards + shardId + allSetups.length) + 1; - const length = tests.length + allSetups.length; - const shard = - shardId === null ? '' : colors.yellow(` [${shardId}:${nbShards}]` + colors.bold(` (${fullIndex}/${length})`)); - console.log(colors.green(`Running "${colors.bold.blue(testName)}" (${colors.bold.white(text)}${shard})...`)); -} - -function printFooter(testName: string, startTime: number) { - // Round to hundredth of a second. - const t = Math.round((Date.now() - startTime) / 10) / 100; - console.log(colors.green('Last step took ') + colors.bold.blue('' + t) + colors.green('s...')); - console.log(''); -} diff --git a/tests/legacy-cli/run_e2e.js b/tests/legacy-cli/run_e2e.js deleted file mode 100644 index fb004c88a042..000000000000 --- a/tests/legacy-cli/run_e2e.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; -require('../../lib/bootstrap-local'); -require('./e2e_runner.ts'); diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 000000000000..17660ff2192e --- /dev/null +++ b/tests/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "@types/tar-stream": "3.1.4", + "@angular-devkit/schematics": "workspace:*", + "tar-stream": "3.1.7", + "tree-kill": "1.2.2" + } +} diff --git a/tests/rollup.config.mjs b/tests/rollup.config.mjs new file mode 100644 index 000000000000..c48299094fef --- /dev/null +++ b/tests/rollup.config.mjs @@ -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 { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import glob from 'fast-glob'; + +const testFiles = [ + 'e2e_runner.js', + 'e2e/utils/test_process.js', + ...glob.sync('e2e/(initialize|setup|tests)/**/*.js'), +]; + +// Generate chunks to keep the original folder structure. +// Needed as we dynamically load these files. +const chunks = {}; +for (const file of testFiles) { + chunks[file.slice(0, -'.js'.length)] = file; +} + +export default { + input: chunks, + external: ['undici', 'puppeteer'], // This cannot be bundled as `node:sqlite` is experimental in node.js 22. Remove once this feature is no longer behind a flag + plugins: [ + nodeResolve({ + preferBuiltins: true, + browser: false, + }), + json(), + commonjs({ + // Test runner uses dynamic requires, and those are fine. + // Rollup should not try to process them. + ignoreDynamicRequires: true, + }), + ], + output: { + dir: './runner_bundled_out', + exports: 'auto', + }, +}; diff --git a/tests/schematics/update/packages/update-migrations/v1_5.js b/tests/schematics/update/packages/update-migrations/v1_5.js index 0be7c20c1fff..becf300fecdf 100644 --- a/tests/schematics/update/packages/update-migrations/v1_5.js +++ b/tests/schematics/update/packages/update-migrations/v1_5.js @@ -3,11 +3,11 @@ * Copyright Google LLC All Rights 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 */ -exports.default = function() { - return function(tree) { - tree.create('/version1_5', ''); - }; +exports.default = function () { + return function (tree) { + tree.create('/version1_5', ''); + }; }; diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 000000000000..5070cc5b6927 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig-test.json", + "compilerOptions": { "paths": {} }, + "exclude": ["e2e/assets/**"] +} diff --git a/tests/legacy-cli/verdaccio.yaml b/tests/verdaccio.yaml similarity index 81% rename from tests/legacy-cli/verdaccio.yaml rename to tests/verdaccio.yaml index f19bc9332694..a398f671b43e 100644 --- a/tests/legacy-cli/verdaccio.yaml +++ b/tests/verdaccio.yaml @@ -3,7 +3,7 @@ storage: ./storage auth: auth-memory: users: {} - +listen: localhost:${HTTP_PORT} uplinks: npmjs: url: https://registry.npmjs.org/ @@ -17,10 +17,10 @@ uplinks: maxFreeSockets: 8 packages: - '@angular/{cli,pwa}': + '@angular/{build,create,cli,pwa,ssr}': access: $all publish: $all - + '@angular-devkit/*': access: $all publish: $all @@ -41,9 +41,11 @@ packages: access: $all proxy: npmjs -logs: - - {type: stdout, format: pretty, level: warn} +log: + type: stdout + format: pretty + level: warn # https://github.com/verdaccio/verdaccio/issues/301 server: - keepAliveTimeout: 0 \ No newline at end of file + keepAliveTimeout: 0 diff --git a/tests/verdaccio_auth.yaml b/tests/verdaccio_auth.yaml new file mode 100644 index 000000000000..1cb53f5d8b5f --- /dev/null +++ b/tests/verdaccio_auth.yaml @@ -0,0 +1,33 @@ +storage: ./storage +auth: + auth-memory: + users: + testing: + name: testing + password: s3cret +listen: localhost:${HTTPS_PORT} +uplinks: + local: + url: http://localhost:${HTTP_PORT} + cache: false + maxage: 20m + max_fails: 32 + timeout: 60s + agent_options: + keepAlive: true + maxSockets: 32 + maxFreeSockets: 8 + +packages: + '**': + access: $authenticated + proxy: local + +log: + type: stdout + format: pretty + level: warn + +# https://github.com/verdaccio/verdaccio/issues/301 +server: + keepAliveTimeout: 0 diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 23428d243f2f..4804a9ee59a6 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -1,13 +1,20 @@ -# 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 -# @external_begin -load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("//tools:defaults.bzl", "js_binary") package(default_visibility = ["//visibility:public"]) -nodejs_binary( +platform( + name = "windows_x64", + constraint_values = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], +) + +exports_files([ + "package_json_release_filter.jq", +]) + +js_binary( name = "ng_cli_schema", data = [ "ng_cli_schema_generator.js", @@ -15,22 +22,11 @@ nodejs_binary( entry_point = "ng_cli_schema_generator.js", ) -nodejs_binary( +js_binary( name = "quicktype_runner", data = [ "quicktype_runner.js", - "@npm//quicktype-core", + "//:node_modules/quicktype-core", ], entry_point = "quicktype_runner.js", - templated_args = ["--bazel_patch_module_resolver"], ) - -platform( - name = "rbe_platform_with_network_access", - exec_properties = { - "dockerNetwork": "standard", - }, - parents = ["@npm//@angular/dev-infra-private/bazel/remote-execution:platform"], -) - -# @external_end diff --git a/packages/schematics/angular/application/files/src/assets/.gitkeep.template b/tools/bazel/BUILD.bazel similarity index 100% rename from packages/schematics/angular/application/files/src/assets/.gitkeep.template rename to tools/bazel/BUILD.bazel diff --git a/tools/bazel/npm_package.bzl b/tools/bazel/npm_package.bzl new file mode 100644 index 000000000000..c6ce650d3971 --- /dev/null +++ b/tools/bazel/npm_package.bzl @@ -0,0 +1,125 @@ +load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin") +load("@aspect_bazel_lib//lib:expand_template.bzl", "expand_template") +load("@aspect_bazel_lib//lib:jq.bzl", "jq") +load("@aspect_bazel_lib//lib:utils.bzl", "to_label") +load("@aspect_rules_js//npm:defs.bzl", _npm_package = "npm_package") +load("@rules_pkg//:pkg.bzl", "pkg_tar") +load("//tools:link_package_json_to_tarballs.bzl", "link_package_json_to_tarballs") +load("//tools:snapshot_repo_filter.bzl", "SNAPSHOT_REPO_JQ_FILTER") +load("//tools:substitutions.bzl", "substitutions") + +def npm_package( + name, + deps = [], + visibility = None, + pkg_deps = [], + stamp_files = [], + pkg_json = "package.json", + extra_substitutions = {}, + replace_prefixes = {}, + **kwargs): + if name != "pkg": + fail("Expected npm_package to be named `pkg`. " + + "This is needed for pnpm workspace integration.") + + # Merge package.json with root package.json and perform various substitutions to + # prepare it for release. For jq docs, see https://stedolan.github.io/jq/manual/. + jq( + name = "basic_substitutions", + # Note: this jq filter relies on the order of the inputs + # buildifier: do not sort + srcs = ["//:package.json", pkg_json], + filter_file = "//tools:package_json_release_filter.jq", + args = ["--slurp"], + out = "substituted/package.json", + ) + + # Copy package.json files to bazel-out so we can use their bazel-out paths to determine + # the corresponding package npm package tgz path for substitutions. + copy_to_bin( + name = "package_json_copy", + srcs = [pkg_json], + ) + pkg_deps_copies = [] + for pkg_dep in pkg_deps: + pkg_label = to_label(pkg_dep) + if pkg_label.name != "package.json": + fail("ERROR: only package.json files allowed in pkg_deps of pkg_npm macro") + pkg_deps_copies.append("@@%s//%s:package_json_copy" % (pkg_label.repo_name, pkg_label.package)) + + # Substitute dependencies on other packages in this repo with tarballs. + link_package_json_to_tarballs( + name = "tar_substitutions", + src = "substituted/package.json", + pkg_deps = [":package_json_copy"] + pkg_deps_copies, + out = "substituted_with_tars/package.json", + ) + + # Substitute dependencies on other packages in this repo with snapshot repos. + jq( + name = "snapshot_repo_substitutions", + srcs = ["substituted/package.json"], + filter = SNAPSHOT_REPO_JQ_FILTER, + out = "substituted_with_snapshot_repos/package.json", + ) + + nostamp_subs = dict(substitutions["nostamp"], **extra_substitutions) + stamp_subs = dict(substitutions["stamp"], **extra_substitutions) + + expand_template( + name = "final_package_json", + template = select({ + # Do local tar substitution if config_setting is true. + "//:package_json_use_tar_deps": "substituted_with_tars/package.json", + # Do snapshot repo substitution if config_setting is true. + "//:package_json_use_snapshot_repo_deps": "substituted_with_snapshot_repos/package.json", + "//conditions:default": "substituted/package.json", + }), + out = "substituted_final/package.json", + substitutions = nostamp_subs, + stamp_substitutions = stamp_subs, + ) + + stamp_targets = [] + for f in stamp_files: + expand_template( + name = "stamp_file_%s" % f, + template = f, + out = "substituted/%s" % f, + substitutions = nostamp_subs, + stamp_substitutions = stamp_subs, + ) + + stamp_targets.append("stamp_file_%s" % f) + + _npm_package( + name = "npm_package", + visibility = visibility, + # Note: Order matters here! Last file takes precedence after replaced prefixes. + srcs = deps + stamp_targets + [":final_package_json"], + replace_prefixes = dict({ + "substituted_final/": "", + "substituted_with_tars/": "", + "substituted_with_snapshot_repos/": "", + "substituted/": "", + }, **replace_prefixes), + allow_overwrites = True, + **kwargs + ) + + # Note: For now, in hybrid mode with RNJS and RJS, we ensure + # both `:pkg` and `:npm_package` work. + native.alias( + name = "pkg", + actual = ":npm_package", + ) + + if pkg_json: + pkg_tar( + name = "npm_package_archive", + srcs = [":pkg"], + extension = "tgz", + # should not be built unless it is a dependency of another rule + tags = ["manual"], + visibility = visibility, + ) diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 29a0d95baec9..d301591a32ba 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -1,38 +1,83 @@ -"""Re-export of some bazel rules with repository-wide defaults.""" +load("@aspect_bazel_lib//lib:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin") +load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test") +load("@aspect_rules_js//js:defs.bzl", _js_binary = "js_binary") +load("@devinfra//bazel/ts_project:index.bzl", "strict_deps_test") +load("@rules_angular//src/ng_examples_db:index.bzl", _ng_examples_db = "ng_examples_db") +load("@rules_angular//src/ng_package:index.bzl", _ng_package = "ng_package") +load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project") +load("//tools:substitutions.bzl", "substitutions") +load("//tools/bazel:npm_package.bzl", _npm_package = "npm_package") -load("@npm//@bazel/typescript:index.bzl", _ts_library = "ts_library") - -_DEFAULT_TSCONFIG_TEST = "//:tsconfig-test.json" - -def ts_library( +def ts_project( name, + deps = [], tsconfig = None, testonly = False, - deps = [], - devmode_module = None, - devmode_target = None, + source_map = True, + visibility = None, **kwargs): - """Default values for ts_library""" - if testonly: - # Match the types[] in //packages:tsconfig-test.json - deps.append("@npm//@types/jasmine") - deps.append("@npm//@types/node") - if not tsconfig and testonly: - tsconfig = _DEFAULT_TSCONFIG_TEST - - if not devmode_module: - devmode_module = "commonjs" - if not devmode_target: - devmode_target = "es2018" - - _ts_library( + if tsconfig == None: + tsconfig = "//:test-tsconfig" if testonly else "//:build-tsconfig" + + _ts_project( name = name, testonly = testonly, + declaration = True, + source_map = source_map, + tsconfig = tsconfig, + visibility = visibility, deps = deps, - # @external_begin + **kwargs + ) + + strict_deps_test( + name = "%s_strict_deps_test" % name, + srcs = kwargs.get("srcs", []), tsconfig = tsconfig, - devmode_module = devmode_module, - devmode_target = devmode_target, - # @external_end + deps = deps, + ) + +def npm_package(**kwargs): + _npm_package(**kwargs) + +def copy_to_bin(**kwargs): + _copy_to_bin(**kwargs) + +def js_binary(**kwargs): + _js_binary(**kwargs) + +def ng_package(deps = [], extra_substitutions = {}, **kwargs): + nostamp_subs = dict(substitutions["nostamp"], **extra_substitutions) + stamp_subs = dict(substitutions["stamp"], **extra_substitutions) + + _ng_package( + deps = deps, + license = "//:LICENSE", + substitutions = select({ + "//:stamp": stamp_subs, + "//conditions:default": nostamp_subs, + }), **kwargs ) + +def jasmine_test(data = [], args = [], **kwargs): + # Create relative path to root, from current package dir. Necessary as + # we change the `chdir` below to the package directory. + relative_to_root = "/".join([".."] * len(native.package_name().split("/"))) + + _jasmine_test( + node_modules = "//:node_modules", + chdir = native.package_name(), + args = [ + "--require=%s/node_modules/source-map-support/register.js" % relative_to_root, + # Escape so that the `js_binary` launcher triggers Bash expansion. + "'**/*+(.|_)spec.js'", + "'**/*+(.|_)spec.mjs'", + "'**/*+(.|_)spec.cjs'", + ] + args, + data = data + ["//:node_modules/source-map-support"], + **kwargs + ) + +def ng_examples_db(**kwargs): + _ng_examples_db(**kwargs) diff --git a/tools/link_package_json_to_tarballs.bzl b/tools/link_package_json_to_tarballs.bzl new file mode 100644 index 000000000000..1a8ea5a17486 --- /dev/null +++ b/tools/link_package_json_to_tarballs.bzl @@ -0,0 +1,87 @@ +# 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("@aspect_bazel_lib//lib:jq.bzl", "jq") +load("@aspect_bazel_lib//lib:utils.bzl", "to_label") + +def link_package_json_to_tarballs(name, src, pkg_deps, out): + """Substitute tar paths into a package.json file for the packages it depends on. + + src and pkg_deps must be labels in the bazel-out tree for the derived path to the npm_package_archive.tgz to be correct. + + Args: + name: Name of the rule + src: package.json file to perform substitions on + pkg_deps: package.json files of dependencies to substitute + out: Output package.json file + """ + + src_pkg = to_label(src).package + + # Generate partial jq filters for each dependent package that, when run + # against a package.json file, can replace its dependency with a tar path. + filter_files = [] + for i, pkg_dep in enumerate(pkg_deps): + pkg_dep_name = "%s_%s.name" % (name, i) + pkg_dep_filter = "%s_%s.filter" % (name, i) + jq( + name = "%s_%s_name" % (name, i), + srcs = [pkg_dep], + filter = ".name", + out = pkg_dep_name, + ) + + srcs = [ + pkg_dep_name, + pkg_dep, + ] + + # Add dependent tars as srcs to include them in the dependency graph, except + # for the tar for this package as that would create a circular dependency. + pkg_label = to_label(pkg_dep) + if pkg_label.package != src_pkg: + pkg_tar = "@@%s//%s:npm_package_archive.tgz" % (pkg_label.repo_name, pkg_label.package) + srcs.append(pkg_tar) + + # Deriving the absolute path to the tar in the execroot requries different + # commands depending on whether or not the action is sandboxed. + abs_path_sandbox = "readlink $(execpath {pkg_dep})".format(pkg_dep = pkg_dep) + abs_path_nosandbox = "(cd $$(dirname $(execpath {pkg_dep})) && pwd)".format(pkg_dep = pkg_dep) + + native.genrule( + name = "%s_%s_filter" % (name, i), + srcs = srcs, + cmd = """ + TAR=$$(dirname $$({abs_path_sandbox} || {abs_path_nosandbox}))/npm_package_archive.tgz + PKGNAME=$$(cat $(execpath {pkg_name})) + if [[ "$$TAR" != *bazel-out* ]]; then + echo "ERROR: package.json passed to substitute_tar_deps must be in the output tree. You can use copy_to_bin to copy a source file to the output tree." + exit 1 + fi + echo "(..|objects|select(has($${{PKGNAME}})))[$${{PKGNAME}}] |= \\"file:$${{TAR}}\\"" > $@ + """.format( + pkg_name = pkg_dep_name, + abs_path_sandbox = abs_path_sandbox, + abs_path_nosandbox = abs_path_nosandbox, + ), + outs = [pkg_dep_filter], + ) + filter_files.append(pkg_dep_filter) + + # Combine all of the filter files into a single filter by joining with | + filter = "%s.filter" % name + native.genrule( + name = "%s_filter" % name, + srcs = filter_files, + cmd = "cat $(SRCS) | sed '$$!s#$$# |#' > $@", + outs = [filter], + ) + + # Generate final package.json with tar substitutions using the above filter + jq( + name = name, + srcs = [src], + filter_file = filter, + out = out, + ) diff --git a/tools/ng_cli_schema_generator.bzl b/tools/ng_cli_schema_generator.bzl index 6bffacb296a3..86d9552dd70c 100644 --- a/tools/ng_cli_schema_generator.bzl +++ b/tools/ng_cli_schema_generator.bzl @@ -1,46 +1,13 @@ -# 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 - -# @external_begin -def _cli_json_schema_interface_impl(ctx): - args = [ - ctx.files.src[0].path, - ctx.outputs.json.path, - ] - - ctx.actions.run( - inputs = ctx.files.src + ctx.files.data, - executable = ctx.executable._binary, - outputs = [ctx.outputs.json], - arguments = args, +load("@aspect_rules_js//js:defs.bzl", "js_run_binary") + +def cli_json_schema(name, src, out, data = []): + js_run_binary( + name = name, + outs = [out], + tags = ["schema"], + srcs = [src] + data, + tool = "//tools:ng_cli_schema", + progress_message = "Generating CLI interface from %s" % src, + mnemonic = "NgCliJsonSchema", + args = ["$(rootpath %s)" % src, "$(rootpath %s)" % out], ) - - return [DefaultInfo()] - -cli_json_schema = rule( - _cli_json_schema_interface_impl, - attrs = { - "src": attr.label( - allow_files = [".json"], - mandatory = True, - ), - "out": attr.string( - mandatory = True, - ), - "data": attr.label_list( - allow_files = [".json"], - mandatory = True, - ), - "_binary": attr.label( - default = Label("//tools:ng_cli_schema"), - executable = True, - cfg = "host", - ), - }, - outputs = { - "json": "%{out}", - }, -) -# @external_end diff --git a/tools/ng_cli_schema_generator.js b/tools/ng_cli_schema_generator.js index 08573897b90e..88ea5531c41c 100644 --- a/tools/ng_cli_schema_generator.js +++ b/tools/ng_cli_schema_generator.js @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ const { readFileSync, writeFileSync, mkdirSync } = require('fs'); @@ -39,10 +39,11 @@ function generate(inPath, outPath) { throw new Error(`Error while resolving $ref ${value} in ${nestedSchemaPath}.`); } case '$id': - case '$id': - case '$schema': case 'id': + case '$schema': case 'required': + case 'x-prompt': + case 'x-user-analytics': return undefined; default: return value; @@ -69,7 +70,22 @@ function generate(inPath, outPath) { outPath = resolve(buildWorkspaceDirectory, outPath); mkdirSync(dirname(outPath), { recursive: true }); - writeFileSync(outPath, JSON.stringify(schemaParsed, undefined, 2)); + writeFileSync( + outPath, + JSON.stringify( + schemaParsed, + (key, value) => { + if (key === 'x-deprecated') { + // Needed for IDEs, and will be replaced to 'deprecated' later on. This must be a boolean. + // https://json-schema.org/draft/2020-12/json-schema-validation.html#name-deprecated + return !!value; + } + + return value; + }, + 2, + ).replace(/"x-deprecated"/g, '"deprecated"'), + ); } if (require.main === module) { @@ -85,7 +101,7 @@ if (require.main === module) { generate(inPath, outPath); } catch (error) { console.error('An error happened:'); - console.error(err); + console.error(error); process.exit(127); } } diff --git a/tools/package_json_release_filter.jq b/tools/package_json_release_filter.jq new file mode 100644 index 000000000000..4b5a0eb20c62 --- /dev/null +++ b/tools/package_json_release_filter.jq @@ -0,0 +1,37 @@ +# 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 +# +# This filter combines a subproject package.json with the root package.json +# and performs substitutions to prepare it for release. It should be called +# with the --slurp argument and be passed the root pacakge.json followed by +# the subproject package.json. +# +# See jq docs for filter syntax: https://stedolan.github.io/jq/manual/. + +.[0] as $root +| .[1] as $proj + +# Get the fields from root package.json that should override the project +# package.json, i.e., every field except the following +| ($root + | del(.bin, .description, .dependencies, .name, .main, .peerDependencies, .optionalDependencies, .typings, .version, .private, .workspaces, .resolutions, .scripts, .["ng-update"], .pnpm, .dependenciesMeta) +) as $root_overrides + +# Use the project package.json as a base and override other fields from root +| $proj + $root_overrides + +# Combine keywords from both +| .keywords = ($root.keywords + $proj.keywords | unique) + +# Remove devDependencies +| del(.devDependencies) + +# Add engines; versions substituted via pkg_npm ++ {"engines": {"node": "0.0.0-ENGINES-NODE", "npm": "0.0.0-ENGINES-NPM", "yarn": "0.0.0-ENGINES-YARN"}} + +# Remove all `workspace:` pnpm prefixes. Afterwards we can conveniently rely on +# substitutions from the stamp values. Note that we are doing it this way because +# substitutions can apply to multiple files, and `workspace:` can't be reliably replaced. +| walk(if type == "string" and startswith("workspace:") then sub("workspace:"; "") else . end) diff --git a/tools/quicktype_runner.js b/tools/quicktype_runner.js index 0b8a4749ff6f..c67645b15a50 100644 --- a/tools/quicktype_runner.js +++ b/tools/quicktype_runner.js @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ const fs = require('fs'); @@ -36,9 +36,6 @@ const header = ` // THIS FILE IS AUTOMATICALLY GENERATED. TO UPDATE THIS FILE YOU NEED TO CHANGE THE // CORRESPONDING JSON SCHEMA FILE, THEN RUN devkit-admin build (or bazel build ...). -// tslint:disable:no-global-tslint-disable -// tslint:disable - `; // Footer to add to all files. @@ -63,7 +60,6 @@ class FetchingJSONSchemaStore extends JSONSchemaStore { content = fs.readFileSync(filePath, 'utf-8').trim(); } else if (url.hostname) { try { - const fetch = require('node-fetch'); const response = await fetch(address); content = response.text(); } catch (e) { @@ -130,6 +126,7 @@ async function generate(inPath) { inputData, alphabetizeProperties: true, rendererOptions: { + 'prefer-types': 'true', 'just-types': 'true', 'explicit-unions': 'true', 'acronym-style': 'camel', diff --git a/tools/rebase-pr.js b/tools/rebase-pr.js deleted file mode 100644 index 01e916defaad..000000000000 --- a/tools/rebase-pr.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights 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-console -// ** IMPORTANT ** -// This script cannot use external dependencies because it needs to run before they are installed. - -const util = require('util'); -const https = require('https'); -const child_process = require('child_process'); -const exec = util.promisify(child_process.exec); - -function determineTargetBranch(repository, prNumber) { - const pullsUrl = `https://api.github.com/repos/${repository}/pulls/${prNumber}`; - // GitHub requires a user agent: https://developer.github.com/v3/#user-agent-required - const options = { headers: { 'User-Agent': repository } }; - - return new Promise((resolve, reject) => { - https - .get(pullsUrl, options, (res) => { - const { statusCode } = res; - const contentType = res.headers['content-type']; - - let error; - if (statusCode !== 200) { - error = new Error(`Request Failed.\nStatus Code: ${statusCode}.\nResponse: ${res}.\n' +`); - } else if (!/^application\/json/.test(contentType)) { - error = new Error( - 'Invalid content-type.\n' + `Expected application/json but received ${contentType}`, - ); - } - if (error) { - reject(error); - res.resume(); - return; - } - - res.setEncoding('utf8'); - let rawData = ''; - res.on('data', (chunk) => { - rawData += chunk; - }); - res.on('end', () => { - try { - const parsedData = JSON.parse(rawData); - resolve(parsedData['base']['ref']); - } catch (e) { - reject(e); - } - }); - }) - .on('error', (e) => { - reject(e); - }); - }); -} - -if (process.argv.length != 4) { - console.error(`This script requires the GitHub repository and PR number as arguments.`); - console.error(`Example: node scripts/rebase-pr.js angular/angular 123`); - process.exitCode = 1; - return; -} - -const repository = process.argv[2]; -const prNumber = process.argv[3]; -let targetBranch; - -return Promise.resolve() - .then(() => { - console.log(`Determining target branch for PR ${prNumber} on ${repository}.`); - return determineTargetBranch(repository, prNumber); - }) - .then((target) => { - targetBranch = target; - console.log(`Target branch is ${targetBranch}.`); - }) - .then(() => { - console.log(`Fetching ${targetBranch} from origin.`); - return exec(`git fetch origin ${targetBranch}`); - }) - .then((target) => { - console.log(`Rebasing current branch on ${targetBranch}.`); - return exec(`git rebase origin/${targetBranch}`); - }) - .then(() => console.log('Rebase successfull.')) - .catch((err) => { - console.log('Failed to rebase on top or target branch.\n'); - console.error(err); - process.exitCode = 1; - }); diff --git a/tools/snapshot_repo_filter.bzl b/tools/snapshot_repo_filter.bzl new file mode 100644 index 000000000000..a648e8e300a7 --- /dev/null +++ b/tools/snapshot_repo_filter.bzl @@ -0,0 +1,28 @@ +# 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("//:constants.bzl", "SNAPSHOT_REPOS") + +def _generate_snapshot_repo_filter(): + individual_pkg_filters = [] + for pkg_name, snapshot_repo in SNAPSHOT_REPOS.items(): + individual_pkg_filters.append( + """ + . as $root + | [paths(..)] + | [(.[] | select( + contains(["{pkg_name}"]) and + contains(["peerDependenciesMeta"]) != true))] as $paths + | $paths | reduce $paths[] as $path ($root; setpath($path; "github:{snapshot_repo}#BUILD_SCM_HASH-PLACEHOLDER")) | . + """.format( + pkg_name = pkg_name, + snapshot_repo = snapshot_repo, + ), + ) + + return " | ".join(individual_pkg_filters) + +# jq filter that replaces package.json dependencies with snapshot repos +SNAPSHOT_REPO_JQ_FILTER = _generate_snapshot_repo_filter() diff --git a/tools/substitutions.bzl b/tools/substitutions.bzl new file mode 100644 index 000000000000..dc40701971ff --- /dev/null +++ b/tools/substitutions.bzl @@ -0,0 +1,37 @@ +load( + "//:constants.bzl", + "ANGULAR_FW_PEER_DEP", + "ANGULAR_FW_VERSION", + "BASELINE_DATE", + "NG_PACKAGR_PEER_DEP", + "NG_PACKAGR_VERSION", + "RELEASE_ENGINES_NODE", + "RELEASE_ENGINES_NPM", + "RELEASE_ENGINES_YARN", +) + +_stamp_substitutions = { + # Version of the local package being built, generated via the `--workspace_status_command` flag. + "0.0.0-PLACEHOLDER": "{{STABLE_PROJECT_VERSION}}", + "0.0.0-EXPERIMENTAL-PLACEHOLDER": "{{STABLE_PROJECT_EXPERIMENTAL_VERSION}}", + # --- + "BUILD_SCM_HASH-PLACEHOLDER": "{{BUILD_SCM_ABBREV_HASH}}", + "BASELINE-DATE-PLACEHOLDER": BASELINE_DATE, + "0.0.0-ENGINES-NODE": RELEASE_ENGINES_NODE, + "0.0.0-ENGINES-NPM": RELEASE_ENGINES_NPM, + "0.0.0-ENGINES-YARN": RELEASE_ENGINES_YARN, + "0.0.0-NG-PACKAGR-VERSION": NG_PACKAGR_VERSION, + "0.0.0-NG-PACKAGR-PEER-DEP": NG_PACKAGR_PEER_DEP, + "0.0.0-ANGULAR-FW-VERSION": ANGULAR_FW_VERSION, + "0.0.0-ANGULAR-FW-PEER-DEP": ANGULAR_FW_PEER_DEP, +} + +_no_stamp_substitutions = dict(_stamp_substitutions, **{ + "0.0.0-PLACEHOLDER": "0.0.0", + "0.0.0-EXPERIMENTAL-PLACEHOLDER": "0.0.0", +}) + +substitutions = { + "stamp": _stamp_substitutions, + "nostamp": _no_stamp_substitutions, +} diff --git a/tools/test/BUILD.bazel b/tools/test/BUILD.bazel new file mode 100644 index 000000000000..5d210ff7ac50 --- /dev/null +++ b/tools/test/BUILD.bazel @@ -0,0 +1,32 @@ +load("@aspect_bazel_lib//lib:jq.bzl", "jq") +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") + +jq( + name = "final_package_json", + # This jq filter relies on the order of the inputs + # buildifier: do not sort + srcs = [ + "root_package.json", + "project_package.json", + ], + args = [ + "--slurp", + ], + filter_file = "//tools:package_json_release_filter.jq", +) + +# jq outputs CR on windows https://github.com/stedolan/jq/issues/92 +# strip the CRs to do a correct comparison on all platforms +genrule( + name = "final_package_json_cr_stripped", + srcs = [":final_package_json"], + outs = ["final_package_json_cr_stripped.json"], + cmd = "cat $(execpath :final_package_json) | sed \"s#\\r##\" > $@", +) + +# Test correctness of the filter that prepares each project's package.json file for release +diff_test( + name = "package_json_filter_test", + file1 = "expected_package.json", + file2 = ":final_package_json_cr_stripped", +) diff --git a/tools/test/expected_package.json b/tools/test/expected_package.json new file mode 100644 index 000000000000..6630c9062f8a --- /dev/null +++ b/tools/test/expected_package.json @@ -0,0 +1,42 @@ +{ + "name": "project", + "version": "0.0.0-SNAPSHOT", + "description": "Project package.json", + "main": "project/index.js", + "bin": { + "projectfoo": "./bin/project-foo.js" + }, + "keywords": [ + "a", + "b", + "c" + ], + "scripts": { + "build": "node project-build-script" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular-cli.git" + }, + "author": "Angular Authors", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular-cli/issues" + }, + "homepage": "https://github.com/angular/angular-cli", + "dependencies": { + "@project/foo": "1.0.0", + "@project/bar": "2.0.0" + }, + "ng-update": { + "migrations": "@project/migration-collection.json", + "packageGroup": { + "@project/abc": "0.0.0" + } + }, + "engines": { + "node": "0.0.0-ENGINES-NODE", + "npm": "0.0.0-ENGINES-NPM", + "yarn": "0.0.0-ENGINES-YARN" + } +} diff --git a/tools/test/project_package.json b/tools/test/project_package.json new file mode 100644 index 000000000000..894c9f4cf3f2 --- /dev/null +++ b/tools/test/project_package.json @@ -0,0 +1,39 @@ +{ + "name": "project", + "version": "0.0.0-SNAPSHOT", + "description": "Project package.json", + "main": "project/index.js", + "bin": { + "projectfoo": "./bin/project-foo.js" + }, + "keywords": [ + "b", + "c" + ], + "scripts": { + "build": "node project-build-script" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular-cli.git" + }, + "author": "Angular Authors", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular-cli/issues" + }, + "homepage": "https://github.com/angular/angular-cli", + "dependencies": { + "@project/foo": "1.0.0", + "@project/bar": "2.0.0" + }, + "devDependencies": { + "@project/devdep": "1.2.3" + }, + "ng-update": { + "migrations": "@project/migration-collection.json", + "packageGroup": { + "@project/abc": "0.0.0" + } + } +} diff --git a/tools/test/root_package.json b/tools/test/root_package.json new file mode 100644 index 000000000000..e0b263aef042 --- /dev/null +++ b/tools/test/root_package.json @@ -0,0 +1,37 @@ +{ + "name": "root", + "version": "1.0.0-next.1", + "private": true, + "description": "Root package.json", + "bin": { + "root-foo": "./bin/root-foo.js", + "root-bar": "./bin/root-bar.js" + }, + "keywords": [ + "a", + "b" + ], + "scripts": { + "build": "node root-build-script" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular-cli.git" + }, + "author": "Angular Authors", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular-cli/issues" + }, + "homepage": "https://github.com/angular/angular-cli", + "workspaces": { + "packages": ["packages/root/foo/*", "packages/root/bar/*"] + }, + "resolutions": { + "root/foo/bar": "1.0.0" + }, + "devDependencies": { + "@root/foo": "1.0.0", + "@root/bar": "2.0.0" + } +} diff --git a/tools/toolchain_info.bzl b/tools/toolchain_info.bzl new file mode 100644 index 000000000000..727a02abcae4 --- /dev/null +++ b/tools/toolchain_info.bzl @@ -0,0 +1,31 @@ +# look at the toolchains registered in the workspace file with nodejs_register_toolchains + +# the name can be anything the user wants this is just added to the target to create unique names +# the order will match against the order in the TOOLCHAIN_VERSION list. +TOOLCHAINS_NAMES = [ + "node20", + "node22", + "node24", +] + +# this is the list of toolchains that should be used and are registered with nodejs_register_toolchains in the WORKSPACE file +TOOLCHAINS_VERSIONS = [ + select({ + "@bazel_tools//src/conditions:linux_x86_64": "@node20_linux_amd64//:node_toolchain", + "@bazel_tools//src/conditions:darwin": "@node20_darwin_amd64//:node_toolchain", + "@bazel_tools//src/conditions:windows": "@node20_windows_amd64//:node_toolchain", + }), + select({ + "@bazel_tools//src/conditions:linux_x86_64": "@node22_linux_amd64//:node_toolchain", + "@bazel_tools//src/conditions:darwin": "@node22_darwin_amd64//:node_toolchain", + "@bazel_tools//src/conditions:windows": "@node22_windows_amd64//:node_toolchain", + }), + select({ + "@bazel_tools//src/conditions:linux_x86_64": "@node24_linux_amd64//:node_toolchain", + "@bazel_tools//src/conditions:darwin": "@node24_darwin_amd64//:node_toolchain", + "@bazel_tools//src/conditions:windows": "@node24_windows_amd64//:node_toolchain", + }), +] + +# A default toolchain for use when only one is necessary +DEFAULT_TOOLCHAIN_VERSION = TOOLCHAINS_VERSIONS[len(TOOLCHAINS_VERSIONS) - 1] diff --git a/tools/toolchains/BUILD.bazel b/tools/toolchains/BUILD.bazel new file mode 100644 index 000000000000..1abb54e5faa7 --- /dev/null +++ b/tools/toolchains/BUILD.bazel @@ -0,0 +1,44 @@ +load("@rules_cc//cc:defs.bzl", "cc_toolchain") +load(":dummy_cc_toolchain.bzl", "dummy_cc_toolchain_config") + +# This is needed following https://github.com/bazel-contrib/rules_nodejs/pull/3859 +toolchain( + name = "node22_windows_no_exec_toolchain", + exec_compatible_with = [], + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + toolchain = "@node22_windows_amd64//:toolchain", + toolchain_type = "@rules_nodejs//nodejs:toolchain_type", +) + +# This defines a dummy C++ toolchain for Windows. +# Without this, the build fails with "Unable to find a CC toolchain using toolchain resolution". +dummy_cc_toolchain_config(name = "dummy_cc_toolchain_config") + +filegroup(name = "empty") + +cc_toolchain( + name = "dummy_cc_toolchain", + all_files = ":empty", + compiler_files = ":empty", + dwp_files = ":empty", + linker_files = ":empty", + objcopy_files = ":empty", + strip_files = ":empty", + supports_param_files = 0, + toolchain_config = ":dummy_cc_toolchain_config", + toolchain_identifier = "dummy_cc_toolchain", +) + +toolchain( + name = "dummy_cc_windows_no_exec_toolchain", + exec_compatible_with = [], + target_compatible_with = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + toolchain = ":dummy_cc_toolchain", + toolchain_type = "@rules_cc//cc:toolchain_type", +) diff --git a/tools/toolchains/dummy_cc_toolchain.bzl b/tools/toolchains/dummy_cc_toolchain.bzl new file mode 100644 index 000000000000..a18a9c780915 --- /dev/null +++ b/tools/toolchains/dummy_cc_toolchain.bzl @@ -0,0 +1,28 @@ +""" +This file defines a dummy C++ toolchain for Windows. +It is needed to satisfy Bazel's toolchain resolution when cross-compiling for Windows on Linux. +Some rules (e.g. rules_nodejs, js_test) or their dependencies may trigger C++ toolchain resolution +even if no actual C++ compilation is performed for the target platform. +Without this, the build fails with "Unable to find a CC toolchain using toolchain resolution". +""" + +load("@rules_cc//cc:defs.bzl", "cc_common") + +def _impl(ctx): + return cc_common.create_cc_toolchain_config_info( + ctx = ctx, + toolchain_identifier = "dummy-toolchain", + host_system_name = "local", + target_system_name = "local", + target_cpu = "x64_windows", + target_libc = "unknown", + compiler = "dummy", + abi_version = "unknown", + abi_libc_version = "unknown", + ) + +dummy_cc_toolchain_config = rule( + implementation = _impl, + attrs = {}, + provides = [CcToolchainConfigInfo], +) diff --git a/tools/ts_json_schema.bzl b/tools/ts_json_schema.bzl index f0e9fa773fb5..dab651d0d7e0 100644 --- a/tools/ts_json_schema.bzl +++ b/tools/ts_json_schema.bzl @@ -1,48 +1,4 @@ -# 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 - -# @external_begin -def _ts_json_schema_interface_impl(ctx): - args = [ - ctx.files.src[0].path, - ctx.outputs.ts.path, - ] - - ctx.actions.run( - inputs = ctx.files.src + ctx.files.data, - executable = ctx.executable._binary, - outputs = [ctx.outputs.ts], - arguments = args, - ) - - return [DefaultInfo()] - -_ts_json_schema_interface = rule( - _ts_json_schema_interface_impl, - attrs = { - "src": attr.label( - allow_files = [".json"], - mandatory = True, - ), - "out": attr.string( - mandatory = True, - ), - "data": attr.label_list( - allow_files = [".json"], - ), - "_binary": attr.label( - default = Label("//tools:quicktype_runner"), - executable = True, - cfg = "host", - ), - }, - outputs = { - "ts": "%{out}", - }, -) -# @external_end +load("@aspect_rules_js//js:defs.bzl", "js_run_binary") # Generates a TS file that contains the interface for a JSON Schema file. Takes a single `src` # argument as input, an optional data field for reference files, and produces a @@ -52,11 +8,13 @@ _ts_json_schema_interface = rule( def ts_json_schema(name, src, data = []): out = src.replace(".json", ".ts") - # @external_begin - _ts_json_schema_interface( + js_run_binary( name = name + ".interface", - src = src, - out = out, - data = data, + outs = [out], + srcs = [src] + data, + tags = ["schema"], + tool = "//tools:quicktype_runner", + progress_message = "Generating TS interface for %s" % src, + mnemonic = "TsJsonSchema", + args = ["$(rootpath %s)" % src, "$(rootpath %s)" % out], ) - # @external_end diff --git a/tsconfig-build-esm.json b/tsconfig-build-esm.json new file mode 100644 index 000000000000..8682ad1fbdc5 --- /dev/null +++ b/tsconfig-build-esm.json @@ -0,0 +1,18 @@ +/** + * Root tsconfig file for use building all Angular packages. Note there is no rootDir + * and therefore any tsconfig in the package directory will need to define its own + * rootDir. + */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "target": "es2022", + "lib": ["es2022"], + // don't auto-discover @types/node, it results in a ///= 8" - -"@octokit/types@^6.0.0", "@octokit/types@^6.0.3", "@octokit/types@^6.16.1": - version "6.16.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.16.2.tgz#62242e0565a3eb99ca2fd376283fe78b4ea057b4" - integrity sha512-wWPSynU4oLy3i4KGyk+J1BLwRKyoeW2TwRHgwbDz17WtVFzSK2GOErGliruIx8c+MaYtHSYTx36DSmLNoNbtgA== - dependencies: - "@octokit/openapi-types" "^7.2.3" - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= - -"@rollup/plugin-commonjs@^19.0.0": - version "19.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-19.0.0.tgz#8c3e71f9a66908e60d70cc1be205834ef3e45f71" - integrity sha512-adTpD6ATGbehdaQoZQ6ipDFhdjqsTgpOAhFiPwl+dzre4pPshsecptDPyEFb61JMJ1+mGljktaC4jI8ARMSNyw== - dependencies: - "@rollup/pluginutils" "^3.1.0" - commondir "^1.0.1" - estree-walker "^2.0.1" - glob "^7.1.6" - is-reference "^1.2.1" - magic-string "^0.25.7" - resolve "^1.17.0" - -"@rollup/plugin-json@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" - integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== - dependencies: - "@rollup/pluginutils" "^3.0.8" - -"@rollup/plugin-node-resolve@^13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.0.tgz#352f07e430ff377809ec8ec8a6fd636547162dc4" - integrity sha512-41X411HJ3oikIDivT5OKe9EZ6ud6DXudtfNrGbC4nniaxx2esiWjkLOzgnZsWq1IM8YIeL2rzRGLZLBjlhnZtQ== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.19.0" - -"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" - -"@sindresorhus/is@^2.0.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" - integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== - -"@szmarczak/http-timer@^4.0.0": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" - integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== - dependencies: - defer-to-connect "^2.0.0" - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@trysound/sax@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" - integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== - -"@tsconfig/node10@^1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.7.tgz#1eb1de36c73478a2479cc661ef5af1c16d86d606" - integrity sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ== - -"@tsconfig/node12@^1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.7.tgz#677bd9117e8164dc319987dd6ff5fc1ba6fbf18b" - integrity sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A== - -"@tsconfig/node14@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.0.tgz#5bd046e508b1ee90bc091766758838741fdefd6e" - integrity sha512-RKkL8eTdPv6t5EHgFKIVQgsDapugbuOptNd9OOunN/HAkzmmTnZELx1kNCK0rSdUYGmiFMM3rRQMAWiyp023LQ== - -"@tsconfig/node16@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1" - integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA== - -"@types/autoprefixer@^9.0.0": - version "9.7.2" - resolved "https://registry.yarnpkg.com/@types/autoprefixer/-/autoprefixer-9.7.2.tgz#64b3251c9675feef5a631b7dd34cfea50a8fdbcc" - integrity sha512-QX7U7YW3zX3ex6MECtWO9folTGsXeP4b8bSjTq3I1ODM+H+sFHwGKuof+T+qBcDClGlCGtDb3SVfiTVfmcxw4g== - dependencies: - "@types/browserslist" "*" - postcss "7.x.x" - -"@types/babel__core@7.1.14": - version "7.1.14" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" - integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" - integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*", "@types/babel__template@7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.0.tgz#0c888dd70b3ee9eebb6e4f200e809da0076262be" - integrity sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*": - version "7.11.1" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.1.tgz#654f6c4f67568e24c23b367e947098c6206fa639" - integrity sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw== - dependencies: - "@babel/types" "^7.3.0" - -"@types/body-parser@*": - version "1.19.0" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" - integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/browserslist@*": - version "4.15.0" - resolved "https://registry.yarnpkg.com/@types/browserslist/-/browserslist-4.15.0.tgz#ba0265b33003a2581df1fc5f483321a30205f2d2" - integrity sha512-h9LyKErRGZqMsHh9bd+FE8yCIal4S0DxKTOeui56VgVXqa66TKiuaIUxCAI7c1O0LjaUzOTcsMyOpO9GetozRA== - dependencies: - browserslist "*" - -"@types/cacache@^15.0.0": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@types/cacache/-/cacache-15.0.0.tgz#b91c7f35f4aa11233cef8a69c4e24ee5996cdf40" - integrity sha512-SNTTXhHY+2Z1wkxUARvyswbWluo5R5M7spcMXSaOqPbNs1dWJopMa2QaaEh3kvEcOK4LCTZ25PU9LJ3EJmDUSQ== - dependencies: - "@types/node" "*" - -"@types/cacheable-request@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" - integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "*" - "@types/node" "*" - "@types/responselike" "*" - -"@types/caniuse-lite@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/caniuse-lite/-/caniuse-lite-1.0.0.tgz#6506ed4b3f8d19def130d19419062960e86cc3bc" - integrity sha512-g28510gzJpFL0xqvuGAlI+dfIr3qvUcZQVFc7f7u2VlWVqI1oBkWhGLYh3fXfoflR7HRnU4w0NRux0pPJQ7VVg== - -"@types/component-emitter@^1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" - integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== - -"@types/connect-history-api-fallback@*": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.4.tgz#8c0f0e6e5d8252b699f5a662f51bdf82fd9d8bb8" - integrity sha512-Kf8v0wljR5GSCOCF/VQWdV3ZhKOVA73drXtY3geMTQgHy9dgqQ0dLrf31M0hcuWkhFzK5sP0kkS3mJzcKVtZbw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.34" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" - integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== - dependencies: - "@types/node" "*" - -"@types/cookie@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== - -"@types/copy-webpack-plugin@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@types/copy-webpack-plugin/-/copy-webpack-plugin-8.0.0.tgz#274e36af8c7988decb7a3a943aa14d23ac1ea92e" - integrity sha512-EJ9Nd0a628uwvgCEt7bN4F6f2jA0O+i+ajAyq9F4jRTqJJ0ro13C22GL/bnvnsSoKuN/O93yfXqZWhn2R70b/g== - dependencies: - "@types/node" "*" - tapable "^2.0.0" - webpack "^5.1.0" - -"@types/cors@^2.8.8": - version "2.8.10" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" - integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== - -"@types/debug@^4.1.2": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" - integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== - -"@types/eslint-scope@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" - integrity sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "7.2.13" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53" - integrity sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*": - version "0.0.48" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.48.tgz#18dc8091b285df90db2f25aa7d906cfc394b7f74" - integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew== - -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - -"@types/estree@^0.0.47": - version "0.0.47" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" - integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz#a427278e106bca77b83ad85221eae709a3414d42" - integrity sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@*", "@types/express@^4.16.0": - version "4.17.12" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.12.tgz#4bc1bf3cd0cfe6d3f6f2853648b40db7d54de350" - integrity sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/find-cache-dir@^3.0.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@types/find-cache-dir/-/find-cache-dir-3.2.0.tgz#eaaf331699dccf52c47926e4d4f8f3ed8db33f3c" - integrity sha512-+JeT9qb2Jwzw72WdjU+TSvD5O1QRPWCeRpDJV+guiIq+2hwR0DFGw+nZNbTFjMIVe6Bf4GgAKeB/6Ytx6+MbeQ== - -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/http-cache-semantics@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" - integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== - -"@types/http-proxy@^1.17.4", "@types/http-proxy@^1.17.5": - version "1.17.6" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.6.tgz#62dc3fade227d6ac2862c8f19ee0da9da9fd8616" - integrity sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ== - dependencies: - "@types/node" "*" - -"@types/inquirer@^7.3.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" - integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== - dependencies: - "@types/through" "*" - rxjs "^6.4.0" - -"@types/is-windows@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0" - integrity sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg== - -"@types/istanbul-lib-coverage@^2.0.1": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== - -"@types/jasmine@~3.7.0": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.7.7.tgz#56718af036be3c9f86eca560a22e39440b2b0784" - integrity sha512-yZzGe1d1T0y+imXDZ79F030nn8qbmiwpWKCZKvKN0KbTzwXAVYShUxkIxu1ba+vhIdabTGVGCfbtZC0oOam8TQ== - -"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= - -"@types/karma@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@types/karma/-/karma-6.3.0.tgz#535928aeee6e957464acf17d0a9e928b3aa0c27f" - integrity sha512-NQ6AzL/0UhFKrK69R9aPV1a88MBau7F47Tr3Qwg8IlKALDVfRk9w+8r3VfOso0vrFT/IkztjfAPar090laUMdg== - dependencies: - "@types/node" "*" - log4js "^6.2.1" - -"@types/keyv@*", "@types/keyv@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" - integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== - dependencies: - "@types/node" "*" - -"@types/loader-utils@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-2.0.2.tgz#2999dc2a3330b3ac0b2eaa9e01328b3484ef1112" - integrity sha512-y3UaQ1rIkp2Nzv67Wa/MS7GJM958CDyWkMmnFneTRcWKlaSPreESrwruQ2WhEapQHCV6HJ2Pj62k0BB7mtQNHw== - dependencies: - "@types/node" "*" - "@types/webpack" "^4" - -"@types/long@^4.0.0": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - -"@types/mime@^1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" - integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== - -"@types/minimatch@*", "@types/minimatch@3.0.4", "@types/minimatch@^3.0.3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" - integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== - -"@types/minimist@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" - integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== - -"@types/node-fetch@^2.1.6": - version "2.5.10" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" - integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ== - dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node@*", "@types/node@>= 8", "@types/node@>=10.0.0": - version "15.12.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.1.tgz#9b60797dee1895383a725f828a869c86c6caa5c2" - integrity sha512-zyxJM8I1c9q5sRMtVF+zdd13Jt6RU4r4qfhTd7lQubyThvLfx6yYekWSQjGCGV2Tkecgxnlpl/DNlb6Hg+dmEw== - -"@types/node@^10.1.0": - version "10.17.60" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" - integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== - -"@types/node@~12.12.6": - version "12.12.70" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.70.tgz#adf70b179c3ee17620215ee4cb5c68c95f7c37ec" - integrity sha512-i5y7HTbvhonZQE+GnUM2rz1Bi8QkzxdQmEv1LKOv4nWyaQk/gdeiTApuQR3PDJHX7WomAbpx2wlWSEpxXGZ/UQ== - -"@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== - -"@types/npm-package-arg@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/npm-package-arg/-/npm-package-arg-6.1.0.tgz#88bdfce72f6a3d5fa1053c8d44d655e7850642e4" - integrity sha512-vbt5fb0y1svMhu++1lwtKmZL76d0uPChFlw7kEzyUmTwfmpHRcFb8i0R8ElT69q/L+QLgK2hgECivIAvaEDwag== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/parse5-html-rewriting-stream@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-5.1.2.tgz#919d5bbf69ef61e11d873e7195891c3811491a03" - integrity sha512-7CHY6QlayurvYRST5xatE/ipIueph5V+EW2xU12P0CsNucuwygnuiE4foYsdQUEkhnKrTU62KmikANPnoxiGrg== - dependencies: - "@types/parse5-sax-parser" "*" - -"@types/parse5-sax-parser@*": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/parse5-sax-parser/-/parse5-sax-parser-5.0.1.tgz#f1e26e82bb09e48cb0c16ff6d1e88aea1e538fd5" - integrity sha512-wBEwg10aACLggnb44CwzAA27M1Jrc/8TR16zA61/rKO5XZoi7JSfLjdpXbshsm7wOlM6hpfvwygh40rzM2RsQQ== - dependencies: - "@types/node" "*" - "@types/parse5" "*" - -"@types/parse5@*": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299" - integrity sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA== - -"@types/pidusage@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/pidusage/-/pidusage-2.0.1.tgz#45eb309be947dcfa177957ef662ce2a0a2311d48" - integrity sha512-tYYcz/+5v/EGYT83C0pIXrJGOiVBLksQvxgJboG4nGqx/gZTvq0Ro4SkAjECqMk7L4Ww58VWB4j48qeYh4/YJg== - -"@types/postcss-preset-env@^6.7.1": - version "6.7.2" - resolved "https://registry.yarnpkg.com/@types/postcss-preset-env/-/postcss-preset-env-6.7.2.tgz#8e7b63a8c6d728b816dd9efe2cea09323cd24edb" - integrity sha512-Q+vT1ljfQn0Zr3V6OoqVYmU+diYbKnGqiZ1h/ysr1cFTwWB2LdJrsRMXhi+ppRzeswHD+I2AxFHMrS8koTUjVA== - dependencies: - "@types/autoprefixer" "^9.0.0" - postcss "^7.0.32" - -"@types/progress@2.0.3", "@types/progress@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.3.tgz#7ccbd9c6d4d601319126c469e73b5bb90dfc8ccc" - integrity sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A== - dependencies: - "@types/node" "*" - -"@types/q@^0.0.32": - version "0.0.32" - resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" - integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= - -"@types/qs@*": - version "6.9.6" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" - integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== - -"@types/range-parser@*": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" - integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== - -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - -"@types/resolve@^1.17.1": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.0.tgz#11325a379b6f63b858fed49552fd4178495ee087" - integrity sha512-SFT3jdUNlLkjxUWwH/0QjLiEsV38hjdDX8oMcX9jZAD8KWNzRLdg6INZE7UMz9O86b2BOHzA3dR8nF+DbonX2Q== - -"@types/responselike@*": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - dependencies: - "@types/node" "*" - -"@types/sass@^1.16.0": - version "1.16.0" - resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.16.0.tgz#b41ac1c17fa68ffb57d43e2360486ef526b3d57d" - integrity sha512-2XZovu4NwcqmtZtsBR5XYLw18T8cBCnU2USFHTnYLLHz9fkhnoEMoDsqShJIOFsFhn5aJHjweiUUdTrDGujegA== - dependencies: - "@types/node" "*" - -"@types/selenium-webdriver@^3.0.0": - version "3.0.17" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" - integrity sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw== - -"@types/semver@^7.0.0": - version "7.3.6" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.6.tgz#e9831776f4512a7ba6da53e71c26e5fb67882d63" - integrity sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw== - -"@types/serve-static@*": - version "1.13.9" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" - integrity sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== - -"@types/tapable@^1": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.7.tgz#545158342f949e8fd3bfd813224971ecddc3fac4" - integrity sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ== - -"@types/text-table@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@types/text-table/-/text-table-0.2.1.tgz#39c4d4a058a82f677392dfd09976e83d9b4c9264" - integrity sha512-dchbFCWfVgUSWEvhOkXGS7zjm+K7jCUvGrQkAHPk2Fmslfofp4HQTH2pqnQ3Pw5GPYv0zWa2AQjKtsfZThuemQ== - -"@types/through@*": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" - integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== - dependencies: - "@types/node" "*" - -"@types/uglify-js@*": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124" - integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q== - dependencies: - source-map "^0.6.1" - -"@types/uuid@^8.0.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" - integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== - -"@types/webpack-dev-server@^3.1.7": - version "3.11.4" - resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07" - integrity sha512-DCKORHjqNNVuMIDWFrlljftvc9CL0+09p3l7lBpb8dRqgN5SmvkWCY4MPKxoI6wJgdRqohmoNbptkxqSKAzLRg== - dependencies: - "@types/connect-history-api-fallback" "*" - "@types/express" "*" - "@types/serve-static" "*" - "@types/webpack" "^4" - http-proxy-middleware "^1.0.0" - -"@types/webpack-sources@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.1.0.tgz#8882b0bd62d1e0ce62f183d0d01b72e6e82e8c10" - integrity sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.7.3" - -"@types/webpack-sources@^0.1.5": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81" - integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.6.1" - -"@types/webpack@^4": - version "4.41.29" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.29.tgz#2e66c1de8223c440366469415c50a47d97625773" - integrity sha512-6pLaORaVNZxiB3FSHbyBiWM7QdazAWda1zvAq4SbZObZqHSDbWLi62iFdblVea6SK9eyBIVp5yHhKt/yNQdR7Q== - dependencies: - "@types/node" "*" - "@types/tapable" "^1" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - anymatch "^3.0.0" - source-map "^0.6.0" - -"@types/yauzl@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" - integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.26.1.tgz#b9c7313321cb837e2bf8bebe7acc2220659e67d3" - integrity sha512-aoIusj/8CR+xDWmZxARivZjbMBQTT9dImUtdZ8tVCVRXgBUuuZyM5Of5A9D9arQPxbi/0rlJLcuArclz/rCMJw== - dependencies: - "@typescript-eslint/experimental-utils" "4.26.1" - "@typescript-eslint/scope-manager" "4.26.1" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - lodash "^4.17.21" - regexpp "^3.1.0" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/experimental-utils@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.26.1.tgz#a35980a2390da9232aa206b27f620eab66e94142" - integrity sha512-sQHBugRhrXzRCs9PaGg6rowie4i8s/iD/DpTB+EXte8OMDfdCG5TvO73XlO9Wc/zi0uyN4qOmX9hIjQEyhnbmQ== - dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.26.1" - "@typescript-eslint/types" "4.26.1" - "@typescript-eslint/typescript-estree" "4.26.1" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/parser@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.26.1.tgz#cecfdd5eb7a5c13aabce1c1cfd7fbafb5a0f1e8e" - integrity sha512-q7F3zSo/nU6YJpPJvQveVlIIzx9/wu75lr6oDbDzoeIRWxpoc/HQ43G4rmMoCc5my/3uSj2VEpg/D83LYZF5HQ== - dependencies: - "@typescript-eslint/scope-manager" "4.26.1" - "@typescript-eslint/types" "4.26.1" - "@typescript-eslint/typescript-estree" "4.26.1" - debug "^4.3.1" - -"@typescript-eslint/scope-manager@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.26.1.tgz#075a74a15ff33ee3a7ed33e5fce16ee86689f662" - integrity sha512-TW1X2p62FQ8Rlne+WEShyd7ac2LA6o27S9i131W4NwDSfyeVlQWhw8ylldNNS8JG6oJB9Ha9Xyc+IUcqipvheQ== - dependencies: - "@typescript-eslint/types" "4.26.1" - "@typescript-eslint/visitor-keys" "4.26.1" - -"@typescript-eslint/types@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.26.1.tgz#9e7c523f73c34b04a765e4167ca5650436ef1d38" - integrity sha512-STyMPxR3cS+LaNvS8yK15rb8Y0iL0tFXq0uyl6gY45glyI7w0CsyqyEXl/Fa0JlQy+pVANeK3sbwPneCbWE7yg== - -"@typescript-eslint/typescript-estree@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.26.1.tgz#b2ce2e789233d62283fae2c16baabd4f1dbc9633" - integrity sha512-l3ZXob+h0NQzz80lBGaykdScYaiEbFqznEs99uwzm8fPHhDjwaBFfQkjUC/slw6Sm7npFL8qrGEAMxcfBsBJUg== - dependencies: - "@typescript-eslint/types" "4.26.1" - "@typescript-eslint/visitor-keys" "4.26.1" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/visitor-keys@4.26.1": - version "4.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.26.1.tgz#0d55ea735cb0d8903b198017d6d4f518fdaac546" - integrity sha512-IGouNSSd+6x/fHtYRyLOM6/C+QxMDzWlDtN41ea+flWuSF9g02iqcIlX8wM53JkfljoIjP0U+yp7SiTS1onEkw== - dependencies: - "@typescript-eslint/types" "4.26.1" - eslint-visitor-keys "^2.0.0" - -"@verdaccio/commons-api@10.0.0", "@verdaccio/commons-api@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@verdaccio/commons-api/-/commons-api-10.0.0.tgz#2d7de8722f94181f1a71891fe91198a7c14e6dea" - integrity sha512-UC8wrRI9FvqjfDeB1RijF7aVI0JJhCOI8RkEDibCT/JD8zVngphrNmgSWcjo8Es3lRiu7NugWXDSuggCCeCfUg== - dependencies: - http-errors "1.8.0" - http-status-codes "1.4.0" - -"@verdaccio/file-locking@10.0.0", "@verdaccio/file-locking@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@verdaccio/file-locking/-/file-locking-10.0.0.tgz#3d476a6ba28207c795d49828438e7335166c1cfc" - integrity sha512-2tQUbJF3tQ3CY9grAlpovaF/zu8G56CBYMaeHwMBHo9rAmsJI9i7LfliHGS6Jygbs8vd0cOCPT7vl2CL9T8upw== - dependencies: - lockfile "1.0.4" - -"@verdaccio/local-storage@10.0.6": - version "10.0.6" - resolved "https://registry.yarnpkg.com/@verdaccio/local-storage/-/local-storage-10.0.6.tgz#be485a8107ad84206cf80702d325ca47b7f22f68" - integrity sha512-YEImOMUL56lziS/N3o1YzoOcVGZXpyZclGSonw7XQ1lKQEvEhU06V2+tIdjPgtqIOuH9ZKdPeBsBuN7ILa2qzQ== - dependencies: - "@verdaccio/commons-api" "10.0.0" - "@verdaccio/file-locking" "10.0.0" - "@verdaccio/streams" "10.0.0" - async "3.2.0" - debug "4.3.1" - lodash "4.17.21" - lowdb "1.0.0" - mkdirp "1.0.4" - -"@verdaccio/readme@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@verdaccio/readme/-/readme-10.0.0.tgz#f9627c32b309ace311318b98b2c42226823f6cd7" - integrity sha512-OD3dMnRC8SvhgytEzczMBleN+K/3lMqyWw/epeXvolCpCd7mW/Dl5zSR25GiHh/2h3eTKP/HMs4km8gS1MMLgA== - dependencies: - dompurify "^2.2.6" - jsdom "15.2.1" - marked "^2.0.1" - -"@verdaccio/streams@10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@verdaccio/streams/-/streams-10.0.0.tgz#8b06e1d6f06e906ebda0f1d4089cdb651a533541" - integrity sha512-PqxxY11HhweN6z1lwfn9ydLCdnOkCPpthMZs+SGCDz8Rt6gOyrjJVslV7o4uobDipjD9+hUPpJHDeO33Qt24uw== - -"@verdaccio/ui-theme@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@verdaccio/ui-theme/-/ui-theme-3.1.0.tgz#21108f3c1b97e6db5901509d935e1f4ce475950a" - integrity sha512-NmJOcv25/OtF84YrmYxi31beFde7rt+/y2qlnq0wYR4ZCFRE5TsuqisTVTe1OyJ8D8JwwPMyMSMSMtlMwUfqIQ== - -"@webassemblyjs/ast@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f" - integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - -"@webassemblyjs/floating-point-hex-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c" - integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA== - -"@webassemblyjs/helper-api-error@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4" - integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w== - -"@webassemblyjs/helper-buffer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642" - integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA== - -"@webassemblyjs/helper-numbers@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9" - integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1" - integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA== - -"@webassemblyjs/helper-wasm-section@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b" - integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - -"@webassemblyjs/ieee754@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf" - integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b" - integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf" - integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw== - -"@webassemblyjs/wasm-edit@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78" - integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/helper-wasm-section" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-opt" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - "@webassemblyjs/wast-printer" "1.11.0" - -"@webassemblyjs/wasm-gen@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" - integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wasm-opt@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978" - integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - -"@webassemblyjs/wasm-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754" - integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wast-printer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e" - integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -"@yarnpkg/lockfile@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - -JSONStream@1.3.5, JSONStream@^1.0.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -abab@^2.0.0, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-globals@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - -acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== - -acorn-walk@^6.0.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" - integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== - -acorn@^6.0.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.2.1: - version "8.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.3.0.tgz#1193f9b96c4e8232f00b11a9edff81b2c8b98b88" - integrity sha512-tqPKHZ5CaBJw0Xmy0ZZvLs1qTV+BNFSyvn77ASXkpBNfIRk8ev26fKrD9iLGwGA9zedPao52GSHzq8lyZG0NUw== - -add-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" - integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= - -adjust-sourcemap-loader@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" - integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== - dependencies: - loader-utils "^2.0.0" - regex-parser "^2.2.11" - -adm-zip@^0.4.9: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - -agentkeepalive@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" - integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== - dependencies: - debug "^4.1.0" - depd "^1.1.2" - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-formats@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.0.tgz#96eaf83e38d32108b66d82a9cb0cfa24886cdfeb" - integrity sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.1.0, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@8.6.0, ajv@^8.0.0, ajv@^8.0.1: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.0.tgz#60cc45d9c46a477d80d92c48076d972c342e5720" - integrity sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -alphanum-sort@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - -ansi-colors@4.1.1, ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@^3.0.0, anymatch@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -apache-md5@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/apache-md5/-/apache-md5-1.1.2.tgz#ee49736b639b4f108b6e9e626c6da99306b41692" - integrity sha1-7klza2ObTxCLbp5ibG2pkwa0FpI= - -app-root-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" - integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" - integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= - dependencies: - ast-types-flow "0.0.7" - commander "^2.11.0" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-differ@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" - integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== - -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - -array-find-index@^1.0.1, array-find-index@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-ify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" - integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= - -array-includes@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" - integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - get-intrinsic "^1.1.1" - is-string "^1.0.5" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -array.prototype.flat@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" - integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -arrify@^1.0.0, arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -asap@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -ast-types-flow@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - -async@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - -async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - -async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -atomic-sleep@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" - integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== - -autoprefixer@^9.6.1: - version "9.8.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" - integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - colorette "^1.2.1" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -axobject-query@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" - integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== - dependencies: - ast-types-flow "0.0.7" - -babel-loader@8.2.2: - version "8.2.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" - integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^1.4.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" - integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.2.2" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.2.tgz#7424a1682ee44baec817327710b1b094e5f8f7f5" - integrity sha512-l1Cf8PKk12eEk5QP/NQ6TH8A1pee6wWDJ96WjxrMXFLHLOBFzYM4moG80HFgduVhTqAFez4alnZKEhP/bYHg0A== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - core-js-compat "^3.9.1" - -babel-plugin-polyfill-regenerator@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz#b310c8d642acada348c1fa3b3e6ce0e851bee077" - integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-arraybuffer@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" - integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= - -base64-js@^1.1.2, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64id@2.0.0, base64id@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" - integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -bcryptjs@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" - integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= - -before-after-hook@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" - integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3, bl@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -blocking-proxy@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" - integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== - dependencies: - minimist "^1.2.0" - -body-parser@1.19.0, body-parser@^1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -bootstrap@^4.0.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7" - integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brfs@^1.4.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3" - integrity sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ== - dependencies: - quote-stream "^1.0.1" - resolve "^1.1.5" - static-module "^2.2.0" - through2 "^2.0.0" - -brotli@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.2.tgz#525a9cad4fcba96475d7d388f6aecb13eed52f46" - integrity sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y= - dependencies: - base64-js "^1.1.2" - -browser-or-node@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-or-node/-/browser-or-node-1.3.0.tgz#f2a4e8568f60263050a6714b2cc236bb976647a7" - integrity sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg== - -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - -browserslist@*, browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.1, browserslist@^4.16.6, browserslist@^4.6.4, browserslist@^4.9.1: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== - dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" - escalade "^3.1.1" - node-releases "^1.1.71" - -browserstack@^1.5.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3" - integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw== - dependencies: - https-proxy-agent "^2.2.1" - -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer-equal@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" - integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer@^5.2.1, buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - -builtin-modules@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== - -builtins@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" - integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -c8@~7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/c8/-/c8-7.5.0.tgz#a69439ab82848f344a74bb25dc5dd4e867764481" - integrity sha512-GSkLsbvDr+FIwjNSJ8OwzWAyuznEYGTAd1pzb/Kr0FMLuV4vqYJTyjboDTwmlUNAG6jAU3PFWzqIdKrOt1D8tw== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@istanbuljs/schema" "^0.1.2" - find-up "^5.0.0" - foreground-child "^2.0.0" - furi "^2.0.0" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-report "^3.0.0" - istanbul-reports "^3.0.2" - rimraf "^3.0.0" - test-exclude "^6.0.0" - v8-to-istanbul "^7.1.0" - yargs "^16.0.0" - yargs-parser "^20.0.0" - -cacache@15.2.0, cacache@^15.0.5, cacache@^15.0.6, cacache@^15.2.0: - version "15.2.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.2.0.tgz#73af75f77c58e72d8c630a7a2858cb18ef523389" - integrity sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw== - dependencies: - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cacheable-lookup@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" - integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== - dependencies: - "@types/keyv" "^3.1.1" - keyv "^4.0.0" - -cacheable-request@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" - integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^2.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219: - version "1.0.30001235" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001235.tgz#ad5ca75bc5a1f7b12df79ad806d715a43a5ac4ed" - integrity sha512-zWEwIVqnzPkSAXOUlQnPW2oKoYb2aLQ4Q5ejdjBcnH63rfypaW34CxaeBn1VMya2XaEU3P/R2qHpWyj+l0BT1A== - -canonical-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" - integrity sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.4.2, chokidar@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -circular-dependency-plugin@5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600" - integrity sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ== - -clang-format@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.5.0.tgz#1bd4c47b66a1a02556b192b93f5505e7ccec84fb" - integrity sha512-C1LucFX7E+ABVYcPEbBHM4PYQ2+WInXsqsLpFlQ9cmRfSbk7A7b1I06h/nE4bQ3MsyEkb31jY2gC0Dtc76b4IA== - dependencies: - async "^1.5.2" - glob "^7.0.0" - resolve "^1.1.6" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-progress@^3.7.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.9.0.tgz#25db83447deb812e62d05bac1af9aec5387ef3d4" - integrity sha512-g7rLWfhAo/7pF+a/STFH/xPyosaL1zgADhI0OM83hl3c7S43iGvJWEAV2QuDOnQ8i6EMBj/u4+NTd0d5L+4JfA== - dependencies: - colors "^1.1.2" - string-width "^4.2.0" - -cli-spinners@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" - integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -clipanion@3.0.0-rc.12: - version "3.0.0-rc.12" - resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-3.0.0-rc.12.tgz#8c235961feb4851c437fc4f23a4c4c1387622a4a" - integrity sha512-eCixNguza61+8MXXTu6sYzpA8gPZHZzvay4lpFFpr4KSy+43wsugdiKMNejLS9PVcnSuGf0fy9kYs5R2c7Ejmw== - dependencies: - typanion "^3.3.1" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -codelyzer@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-6.0.2.tgz#25d72eae641e8ff13ffd7d99b27c9c7ad5d7e135" - integrity sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g== - dependencies: - "@angular/compiler" "9.0.0" - "@angular/core" "9.0.0" - app-root-path "^3.0.0" - aria-query "^3.0.0" - axobject-query "2.0.2" - css-selector-tokenizer "^0.7.1" - cssauron "^1.4.0" - damerau-levenshtein "^1.0.4" - rxjs "^6.5.3" - semver-dsl "^1.0.1" - source-map "^0.5.7" - sprintf-js "^1.1.2" - tslib "^1.10.0" - zone.js "~0.10.3" - -collection-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/collection-utils/-/collection-utils-1.0.1.tgz#31d14336488674f27aefc0a7c5eccacf6df78044" - integrity sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg== - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0" - integrity sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA== - -colorette@^1.2.1, colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - -colors@1.4.0, colors@^1.1.2, colors@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^2.11.0, commander@^2.12.1, commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^7.0.0, commander@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -compare-func@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" - integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== - dependencies: - array-ify "^1.0.0" - dot-prop "^5.1.0" - -component-emitter@^1.2.1, component-emitter@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@1.7.4, compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.2, concat-stream@~1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -connect@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -conventional-changelog-angular@^5.0.12: - version "5.0.12" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" - integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-atom@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de" - integrity sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw== - dependencies: - q "^1.5.1" - -conventional-changelog-codemirror@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz#398e9530f08ce34ec4640af98eeaf3022eb1f7dc" - integrity sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw== - dependencies: - q "^1.5.1" - -conventional-changelog-conventionalcommits@^4.5.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.0.tgz#7fc17211dbca160acf24687bd2fdd5fd767750eb" - integrity sha512-sj9tj3z5cnHaSJCYObA9nISf7eq/YjscLPoq6nmew4SiOjxqL2KRpK20fjnjVbpNDjJ2HR3MoVcWKXwbVvzS0A== - dependencies: - compare-func "^2.0.0" - lodash "^4.17.15" - q "^1.5.1" - -conventional-changelog-core@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" - integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== - dependencies: - add-stream "^1.0.0" - conventional-changelog-writer "^4.0.18" - conventional-commits-parser "^3.2.0" - dateformat "^3.0.0" - get-pkg-repo "^1.0.0" - git-raw-commits "^2.0.8" - git-remote-origin-url "^2.0.0" - git-semver-tags "^4.1.1" - lodash "^4.17.15" - normalize-package-data "^3.0.0" - q "^1.5.1" - read-pkg "^3.0.0" - read-pkg-up "^3.0.0" - shelljs "^0.8.3" - through2 "^4.0.0" - -conventional-changelog-ember@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz#619b37ec708be9e74a220f4dcf79212ae1c92962" - integrity sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A== - dependencies: - q "^1.5.1" - -conventional-changelog-eslint@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb" - integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA== - dependencies: - q "^1.5.1" - -conventional-changelog-express@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8" - integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ== - dependencies: - q "^1.5.1" - -conventional-changelog-jquery@^3.0.11: - version "3.0.11" - resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf" - integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw== - dependencies: - q "^1.5.1" - -conventional-changelog-jshint@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff" - integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-preset-loader@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" - integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== - -conventional-changelog-writer@^4.0.18: - version "4.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" - integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== - dependencies: - compare-func "^2.0.0" - conventional-commits-filter "^2.0.7" - dateformat "^3.0.0" - handlebars "^4.7.6" - json-stringify-safe "^5.0.1" - lodash "^4.17.15" - meow "^8.0.0" - semver "^6.0.0" - split "^1.0.0" - through2 "^4.0.0" - -conventional-changelog@^3.0.0: - version "3.1.24" - resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" - integrity sha512-ed6k8PO00UVvhExYohroVPXcOJ/K1N0/drJHx/faTH37OIZthlecuLIRX/T6uOp682CAoVoFpu+sSEaeuH6Asg== - dependencies: - conventional-changelog-angular "^5.0.12" - conventional-changelog-atom "^2.0.8" - conventional-changelog-codemirror "^2.0.8" - conventional-changelog-conventionalcommits "^4.5.0" - conventional-changelog-core "^4.2.1" - conventional-changelog-ember "^2.0.9" - conventional-changelog-eslint "^3.0.9" - conventional-changelog-express "^2.0.6" - conventional-changelog-jquery "^3.0.11" - conventional-changelog-jshint "^2.0.9" - conventional-changelog-preset-loader "^2.3.4" - -conventional-commits-filter@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" - integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== - dependencies: - lodash.ismatch "^4.4.0" - modify-values "^1.0.0" - -conventional-commits-parser@^3.0.0, conventional-commits-parser@^3.2.0, conventional-commits-parser@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.1.tgz#ba44f0b3b6588da2ee9fd8da508ebff50d116ce2" - integrity sha512-OG9kQtmMZBJD/32NEw5IhN5+HnBqVjy03eC+I71I0oQRFA5rOgA4OtPOYG7mz1GkCfCNxn3gKIX8EiHJYuf1cA== - dependencies: - JSONStream "^1.0.4" - is-text-path "^1.0.1" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" - trim-off-newlines "^1.0.0" - -convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -cookie@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - -cookies@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" - integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== - dependencies: - depd "~2.0.0" - keygrip "~1.1.0" - -copy-anything@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.3.tgz#842407ba02466b0df844819bbe3baebbe5d45d87" - integrity sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ== - dependencies: - is-what "^3.12.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -copy-webpack-plugin@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.0.0.tgz#2bf592785d2fcdde9342dfed3676490fe0aa7ce8" - integrity sha512-k8UB2jLIb1Jip2nZbCz83T/XfhfjX6mB1yLJNYKrpYi7FQimfOoFv/0//iT6HV1K8FwUB5yUbCcnpLebJXJTug== - dependencies: - fast-glob "^3.2.5" - glob-parent "^6.0.0" - globby "^11.0.3" - normalize-path "^3.0.0" - p-limit "^3.1.0" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - -core-js-compat@^3.9.0, core-js-compat@^3.9.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.14.0.tgz#b574dabf29184681d5b16357bd33d104df3d29a5" - integrity sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A== - dependencies: - browserslist "^4.16.6" - semver "7.0.0" - -core-js@3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c" - integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA== - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cors@2.8.5, cors@~2.8.5: - version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -critters@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.10.tgz#edd0e962fc5af6c4adb6dbf1a71bae2d3f917000" - integrity sha512-p5VKhP1803+f+0Jq5P03w1SbiHtpAKm+1EpJHkiPxQPq0Vu9QLZHviJ02GRrWi0dlcJqrmzMWInbwp4d22RsGw== - dependencies: - chalk "^4.1.0" - css "^3.0.0" - parse5 "^6.0.1" - parse5-htmlparser2-tree-adapter "^6.0.1" - pretty-bytes "^5.3.0" - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0, cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== - dependencies: - postcss "^7.0.5" - -css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-color-names@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" - integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA== - -css-declaration-sorter@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9" - integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw== - dependencies: - timsort "^0.3.0" - -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" - -css-loader@5.2.6: - version "5.2.6" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.6.tgz#c3c82ab77fea1f360e587d871a6811f4450cc8d1" - integrity sha512-0wyN5vXMQZu6BvjbrPdUJvkCzGEO24HC7IS7nW4llc6BBFC+zwR9CKtYGv63Puzsg10L/o12inMY5/2ByzfD6w== - dependencies: - icss-utils "^5.1.0" - loader-utils "^2.0.0" - postcss "^8.2.15" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^3.0.0" - semver "^7.3.5" - -css-minimizer-webpack-plugin@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.1.tgz#2f866079411d42309a485512642c0cb08b5468ae" - integrity sha512-RGFIv6iZWUPO2T1vE5+5pNCSs2H2xtHYRdfZPiiNH8Of6QOn9BeFnZSoHiQMkmsxOO/JkPe4BpKfs7slFIWcTA== - dependencies: - cssnano "^5.0.0" - jest-worker "^27.0.2" - p-limit "^3.0.2" - postcss "^8.2.9" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - source-map "^0.6.1" - -css-parse@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" - integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= - dependencies: - css "^2.0.0" - -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" - -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== - dependencies: - boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" - nth-check "^2.0.0" - -css-selector-tokenizer@^0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz#735f26186e67c749aaf275783405cf0661fae8f1" - integrity sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg== - dependencies: - cssesc "^3.0.0" - fastparse "^1.1.2" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== - -css@^2.0.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== - dependencies: - inherits "^2.0.3" - source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" - -css@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== - dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" - -cssauron@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" - integrity sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg= - dependencies: - through X.X.X - -cssdb@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-default@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.2.tgz#5d4877a91769823c5da6bcebd54996ecdf8aca12" - integrity sha512-spilp8LRw0sacuxiN9A/dyyPr6G/WISKMBKcBD4NMoPV0ENx4DeuWvIIrSx9PII2nJIDCO3kywkqTPreECBVOg== - dependencies: - css-declaration-sorter "^6.0.3" - cssnano-utils "^2.0.1" - postcss-calc "^8.0.0" - postcss-colormin "^5.2.0" - postcss-convert-values "^5.0.1" - postcss-discard-comments "^5.0.1" - postcss-discard-duplicates "^5.0.1" - postcss-discard-empty "^5.0.1" - postcss-discard-overridden "^5.0.1" - postcss-merge-longhand "^5.0.2" - postcss-merge-rules "^5.0.2" - postcss-minify-font-values "^5.0.1" - postcss-minify-gradients "^5.0.1" - postcss-minify-params "^5.0.1" - postcss-minify-selectors "^5.1.0" - postcss-normalize-charset "^5.0.1" - postcss-normalize-display-values "^5.0.1" - postcss-normalize-positions "^5.0.1" - postcss-normalize-repeat-style "^5.0.1" - postcss-normalize-string "^5.0.1" - postcss-normalize-timing-functions "^5.0.1" - postcss-normalize-unicode "^5.0.1" - postcss-normalize-url "^5.0.1" - postcss-normalize-whitespace "^5.0.1" - postcss-ordered-values "^5.0.1" - postcss-reduce-initial "^5.0.1" - postcss-reduce-transforms "^5.0.1" - postcss-svgo "^5.0.2" - postcss-unique-selectors "^5.0.1" - -cssnano-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" - integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== - -cssnano@^5.0.0: - version "5.0.5" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.5.tgz#6b8787123bf4cd5a220a2fa6cb5bc036b0854b48" - integrity sha512-L2VtPXnq6rmcMC9vkBOP131sZu3ccRQI27ejKZdmQiPDpUlFkUbpXHgKN+cibeO1U4PItxVZp1zTIn5dHsXoyg== - dependencies: - cosmiconfig "^7.0.0" - cssnano-preset-default "^5.1.2" - is-resolvable "^1.1.0" - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -cssom@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== - dependencies: - cssom "~0.3.6" - -cuint@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" - integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= - -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - -damerau-levenshtein@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" - integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== - -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -data-urls@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - -date-format@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" - integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== - -date-format@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95" - integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w== - -dateformat@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" - integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== - -dayjs@1.10.4: - version "1.10.4" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2" - integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw== - -debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@~4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debuglog@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" - integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= - -decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= - dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" - -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" - integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== - dependencies: - mimic-response "^2.0.0" - -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -deep-is@^0.1.3, deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - -defer-to-connect@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@^1.1.2, depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dependency-graph@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" - integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -devtools-protocol@0.0.883894: - version "0.0.883894" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.883894.tgz#d403f2c75cd6d71c916aee8dde9258da988a4da9" - integrity sha512-33idhm54QJzf3Q7QofMgCvIVSd2o9H3kQPWaKT/fhoZh+digc+WSiMhbkeG3iN79WY4Hwr9G05NpbhEVrsOYAg== - -dezalgo@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" - integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= - dependencies: - asap "^2.0.0" - wrappy "1" - -di@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" - integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= - -diff@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" - integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-serialize@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" - integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - -dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" - integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== - dependencies: - domelementtype "^2.2.0" - -domino@^2.1.2: - version "2.1.6" - resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" - integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== - -dompurify@^2.2.6: - version "2.2.9" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.9.tgz#4b42e244238032d9286a0d2c87b51313581d9624" - integrity sha512-+9MqacuigMIZ+1+EwoEltogyWGFTJZWU3258Rupxs+2CGs4H914G9er6pZbsme/bvb5L67o2rade9n21e4RW/w== - -domutils@^2.4.3: - version "2.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" - integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -dot-prop@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexer2@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= - dependencies: - readable-stream "^2.0.2" - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -ejs@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== - dependencies: - jake "^10.6.1" - -electron-to-chromium@^1.3.723: - version "1.3.749" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.749.tgz#0ecebc529ceb49dd2a7c838ae425236644c3439a" - integrity sha512-F+v2zxZgw/fMwPz/VUGIggG4ZndDsYy0vlpthi3tjmDZlcfbhN5mYW0evXUsBr2sUtuDANFtle410A9u/sd/4A== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -encoding@^0.1.11, encoding@^0.1.12: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -engine.io-parser@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e" - integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg== - dependencies: - base64-arraybuffer "0.1.4" - -engine.io@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b" - integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w== - dependencies: - accepts "~1.3.4" - base64id "2.0.0" - cookie "~0.4.1" - cors "~2.8.5" - debug "~4.3.1" - engine.io-parser "~4.0.0" - ws "~7.4.2" - -enhanced-resolve@5.8.2, enhanced-resolve@^5.8.0: - version "5.8.2" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" - integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -ent@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -envinfo@7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -errno@^0.1.1, errno@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: - version "1.18.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" - integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.3" - is-string "^1.0.6" - object-inspect "^1.10.3" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-module-lexer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.1.tgz#dda8c6a14d8f340a24e34331e0fab0cb50438e0e" - integrity sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA== - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: - version "0.10.53" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" - integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.3" - next-tick "~1.0.0" - -es6-iterator@^2.0.3, es6-iterator@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -es6-symbol@^3.1.1, es6-symbol@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-weak-map@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escodegen@^1.11.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -escodegen@~1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" - integrity sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -eslint-config-prettier@8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== - -eslint-import-resolver-node@0.3.4, eslint-import-resolver-node@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" - integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== - dependencies: - debug "^2.6.9" - resolve "^1.13.1" - -eslint-module-utils@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233" - integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A== - dependencies: - debug "^3.2.7" - pkg-dir "^2.0.0" - -eslint-plugin-header@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6" - integrity sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg== - -eslint-plugin-import@2.22.1: - version "2.22.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" - integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== - dependencies: - array-includes "^3.1.1" - array.prototype.flat "^1.2.3" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.4" - eslint-module-utils "^2.6.0" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.1" - read-pkg-up "^2.0.0" - resolve "^1.17.0" - tsconfig-paths "^3.9.0" - -eslint-scope@5.1.1, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint@7.28.0: - version "7.28.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820" - integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.2" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" - -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0, esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1, estraverse@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== - -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - -estree-walker@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-emitter@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= - dependencies: - d "1" - es5-ext "~0.10.14" - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -eventsource@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" - integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== - dependencies: - original "^1.0.0" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -express@4.17.1, express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -ext@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" - integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A== - dependencies: - type "^2.0.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@^3.0.0, extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -falafel@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.2.4.tgz#b5d86c060c2412a43166243cb1bce44d1abd2819" - integrity sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ== - dependencies: - acorn "^7.1.1" - foreach "^2.0.5" - isarray "^2.0.1" - object-keys "^1.0.6" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.1.1, fast-glob@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fast-redact@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.1.tgz#d6015b971e933d03529b01333ba7f22c29961e92" - integrity sha512-kYpn4Y/valC9MdrISg47tZOpYBNoTXKgT9GYXFpHN/jYFs+lFkPoisY+LcBODdKVMY96ATzvzsWv+ES/4Kmufw== - -fast-safe-stringify@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== - -fastparse@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== - -fastq@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" - integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== - dependencies: - minimatch "^3.0.4" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.1.2, finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-cache-dir@3.3.1, find-cache-dir@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" - integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-parent-dir@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.1.tgz#c5c385b96858c3351f95d446cab866cbf9f11125" - integrity sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A== - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatstr@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" - integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== - -flatted@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -flatted@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" - integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== - -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - -follow-redirects@^1.0.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" - integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== - -font-awesome@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" - integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM= - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - -foreground-child@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" - integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^3.0.2" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-minipass@^2.0.0, fs-minipass@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs-monkey@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -furi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/furi/-/furi-2.0.0.tgz#13d85826a1af21acc691da6254b3888fc39f0b4a" - integrity sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ== - dependencies: - "@types/is-windows" "^1.0.0" - is-windows "^1.0.2" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.1, get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= - dependencies: - hosted-git-info "^2.1.4" - meow "^3.3.0" - normalize-package-data "^2.3.0" - parse-github-repo-url "^1.3.0" - through2 "^2.0.0" - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.0.0, get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -gh-got@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-9.0.0.tgz#5f82eb5c97aa7a0235f50cf277331cdeda879670" - integrity sha512-RH5n6CDdb6AozElmiKwFhmO/1FmhWWVhfQAVv+JtU8jtPK12JLErce/VQFsFwZ9dTa01SfD7WXb/1iyZp/5XKg== - dependencies: - got "^10.5.7" - -git-raw-commits@^2.0.0, git-raw-commits@^2.0.10, git-raw-commits@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" - integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== - dependencies: - dargs "^7.0.0" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" - -git-remote-origin-url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" - integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= - dependencies: - gitconfiglocal "^1.0.0" - pify "^2.3.0" - -git-semver-tags@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" - integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== - dependencies: - meow "^8.0.0" - semver "^6.0.0" - -gitconfiglocal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" - integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= - dependencies: - ini "^1.3.2" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.0.tgz#f851b59b388e788f3a44d63fab50382b2859c33c" - integrity sha512-Hdd4287VEJcZXUwv1l8a+vXC1GjOQqXe+VS30w/ypihpcnu9M1n3xeYeJu5CBpeEQj2nAab2xxz28GuA3vp4Ww== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@7.1.7, glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^6.0.1: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.6.0, globals@^13.9.0: - version "13.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.9.0.tgz#4bf2bf635b334a173fb1daf7c5e6b218ecdc06cb" - integrity sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA== - dependencies: - type-fest "^0.20.2" - -globby@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" - integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -got@^10.5.7: - version "10.7.0" - resolved "https://registry.yarnpkg.com/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" - integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== - dependencies: - "@sindresorhus/is" "^2.0.0" - "@szmarczak/http-timer" "^4.0.0" - "@types/cacheable-request" "^6.0.1" - cacheable-lookup "^2.0.0" - cacheable-request "^7.0.1" - decompress-response "^5.0.0" - duplexer3 "^0.1.4" - get-stream "^5.0.0" - lowercase-keys "^2.0.0" - mimic-response "^2.1.0" - p-cancelable "^2.0.0" - p-event "^4.0.0" - responselike "^2.0.0" - to-readable-stream "^2.0.0" - type-fest "^0.10.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -handlebars@4.7.7, handlebars@^4.7.6: - version "4.7.7" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0, har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== - dependencies: - lru-cache "^6.0.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - -html-entities@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" - integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" - integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" - integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy-middleware@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665" - integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== - dependencies: - "@types/http-proxy" "^1.17.5" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.17.0, http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-status-codes@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" - integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ== - -https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= - dependencies: - ms "^2.0.0" - -husky@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" - integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== - -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-walk@^3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" - integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== - dependencies: - minimatch "^3.0.4" - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= - -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.2, ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -injection-js@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.4.0.tgz#ebe8871b1a349f23294eaa751bbd8209a636e754" - integrity sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA== - dependencies: - tslib "^2.0.0" - -inquirer@8.1.0, inquirer@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.0.tgz#68ce5ce5376cf0e89765c993d8b7c1e62e184d69" - integrity sha512-1nKYPoalt1vMBfCMtpomsUc32wmOoWXAoq3kM/5iTfxyQ2f/BxjixQpC+mbZ7BI0JUXHED4/XPXekDVtJNpXYw== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.3.0" - run-async "^2.4.0" - rxjs "^6.6.6" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.1, ipaddr.js@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arguments@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" - integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== - dependencies: - call-bind "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-bigint@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" - integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" - integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== - dependencies: - call-bind "^1.0.2" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== - -is-color-stop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-core-module@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" - integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" - integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= - -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= - -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" - integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= - -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" - integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== - dependencies: - is-path-inside "^1.0.0" - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-promise@^2.1.0, is-promise@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== - -is-reference@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - -is-regex@^1.0.4, is-regex@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" - integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== - dependencies: - call-bind "^1.0.2" - has-symbols "^1.0.2" - -is-resolvable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-stream@^1.0.1, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-string@^1.0.5, is-string@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" - integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-text-path@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" - integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= - dependencies: - text-extensions "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-url@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" - integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-what@^3.12.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" - integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isarray@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isbinaryfile@^4.0.6: - version "4.0.8" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf" - integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isomorphic-fetch@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== - -istanbul-lib-instrument@^4.0.1, istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" - integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.0.0, istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== - dependencies: - async "0.9.x" - chalk "^2.4.2" - filelist "^1.0.1" - minimatch "^3.0.4" - -jasmine-core@^3.6.0, jasmine-core@~3.7.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561" - integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ== - -jasmine-core@~2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" - integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= - -jasmine-reporters@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.4.0.tgz#708c17ae70ba6671e3a930bb1b202aab80a31409" - integrity sha512-jxONSrBLN1vz/8zCx5YNWQSS8iyDAlXQ5yk1LuqITe4C6iXCDx5u6Q0jfNtkKhL4qLZPe69fL+AWvXFt9/x38w== - dependencies: - mkdirp "^0.5.1" - xmldom "^0.5.0" - -jasmine-spec-reporter@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz#94b939448e63d4e2bd01668142389f20f0a8ea49" - integrity sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg== - dependencies: - colors "1.4.0" - -jasmine@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" - integrity sha1-awicChFXax8W3xG4AUbZHU6Lij4= - dependencies: - exit "^0.1.2" - glob "^7.0.6" - jasmine-core "~2.8.0" - -jasmine@^3.3.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.7.0.tgz#d36638c0c815e6ad5666676e386d79e2ccb70835" - integrity sha512-wlzGQ+cIFzMEsI+wDqmOwvnjTvolLFwlcpYLCqSPPH0prOQaW3P+IzMhHYn934l1imNvw07oCyX+vGUv3wmtSQ== - dependencies: - glob "^7.1.6" - jasmine-core "~3.7.0" - -jasminewd2@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" - integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= - -jest-worker@27.0.2, jest-worker@^27.0.2: - version "27.0.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.2.tgz#4ebeb56cef48b3e7514552f80d0d80c0129f0b05" - integrity sha512-EoBdilOTTyOgmHXtw/cPc+ZrCA0KJMrkXzkrPGNwLmnvvlN1nj7MPrxpT7m+otSv2e1TLaVffzDnE/LB14zJMg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jquery@^3.3.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" - integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== - -js-base64@^2.4.3: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@15.2.1: - version "15.2.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" - integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== - dependencies: - abab "^2.0.0" - acorn "^7.1.0" - acorn-globals "^4.3.2" - array-equal "^1.0.0" - cssom "^0.4.1" - cssstyle "^2.0.0" - data-urls "^1.1.0" - domexception "^1.0.1" - escodegen "^1.11.1" - html-encoding-sniffer "^1.0.2" - nwsapi "^2.2.0" - parse5 "5.1.0" - pn "^1.1.0" - request "^2.88.0" - request-promise-native "^1.0.7" - saxes "^3.1.9" - symbol-tree "^3.2.2" - tough-cookie "^3.0.1" - w3c-hr-time "^1.0.1" - w3c-xmlserializer "^1.1.2" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^7.0.0" - ws "^7.0.0" - xml-name-validator "^3.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json3@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.0, json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - -jsonc-parser@3.0.0, jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.2.0, jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= - -jsonwebtoken@8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jszip@^3.1.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" - integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - set-immediate-shim "~1.0.1" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -karma-chrome-launcher@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" - integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg== - dependencies: - which "^1.2.1" - -karma-coverage@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.0.3.tgz#c10f4711f4cf5caaaa668b1d6f642e7da122d973" - integrity sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g== - dependencies: - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.1" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.0" - minimatch "^3.0.4" - -karma-jasmine-html-reporter@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz#586e17025a1b4128e9fba55d5f1e8921bfc3bc1e" - integrity sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw== - -karma-jasmine@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82" - integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw== - dependencies: - jasmine-core "^3.6.0" - -karma-source-map-support@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" - integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== - dependencies: - source-map-support "^0.5.5" - -karma@~6.3.0: - version "6.3.3" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.3.tgz#bd04c7c533f8de99b3c3e85e191d0a6ae2621ad1" - integrity sha512-JRAujkKWaOtO2LmyPH7K2XXRhrxuFAn9loIL9+iiah6vjz+ZLkqdKsySV9clRITGhj10t9baIfbCl6CJ5hu9gQ== - dependencies: - body-parser "^1.19.0" - braces "^3.0.2" - chokidar "^3.4.2" - colors "^1.4.0" - connect "^3.7.0" - di "^0.0.1" - dom-serialize "^2.2.1" - glob "^7.1.6" - graceful-fs "^4.2.4" - http-proxy "^1.18.1" - isbinaryfile "^4.0.6" - lodash "^4.17.19" - log4js "^6.2.1" - mime "^2.4.5" - minimatch "^3.0.4" - qjobs "^1.2.0" - range-parser "^1.2.1" - rimraf "^3.0.2" - socket.io "^3.1.0" - source-map "^0.6.1" - tmp "0.2.1" - ua-parser-js "^0.7.23" - yargs "^16.1.1" - -keygrip@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" - integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== - dependencies: - tsscmp "1.0.6" - -keyv@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" - integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== - dependencies: - json-buffer "3.0.1" - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" - integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== - -klona@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== - -less-loader@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-9.0.0.tgz#71a0b530174bddf89bb11a5019dd725f54df4791" - integrity sha512-bPen1xeGTZuYFFobcdz9kMUVgSSSDZQJtyhawtCtcz1QboQOwhkI7uCwp5UO+IZpO+LJS1W73YwxsufbBT6SBQ== - dependencies: - klona "^2.0.4" - -less@4.1.1, less@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/less/-/less-4.1.1.tgz#15bf253a9939791dc690888c3ff424f3e6c7edba" - integrity sha512-w09o8tZFPThBscl5d0Ggp3RcrKIouBoQscnOMgFH3n5V3kN/CXGHNfCkRPtxJk6nKryDXaV9aHLK55RXuH4sAw== - dependencies: - copy-anything "^2.0.1" - parse-node-version "^1.0.1" - tslib "^1.10.0" - optionalDependencies: - errno "^0.1.1" - graceful-fs "^4.1.2" - image-size "~0.5.0" - make-dir "^2.1.0" - mime "^1.4.1" - needle "^2.5.2" - source-map "~0.6.0" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -license-checker@^25.0.0: - version "25.0.1" - resolved "https://registry.yarnpkg.com/license-checker/-/license-checker-25.0.1.tgz#4d14504478a5240a857bb3c21cd0491a00d761fa" - integrity sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g== - dependencies: - chalk "^2.4.1" - debug "^3.1.0" - mkdirp "^0.5.1" - nopt "^4.0.1" - read-installed "~4.0.3" - semver "^5.5.0" - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - spdx-satisfies "^4.0.0" - treeify "^1.1.0" - -license-webpack-plugin@2.3.19: - version "2.3.19" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.19.tgz#f02720b2b0bcd9ae27fb63f0bd908d9ac9335d6c" - integrity sha512-z/izhwFRYHs1sCrDgrTUsNJpd+Xsd06OcFWSwHz/TiZygm5ucweVZi1Hu14Rf6tOj/XAl1Ebyc7GW6ZyyINyWA== - dependencies: - "@types/webpack-sources" "^0.1.5" - webpack-sources "^1.2.0" - -lie@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== - dependencies: - immediate "~3.0.5" - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== - -loader-utils@2.0.0, loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lockfile@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" - integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA== - dependencies: - signal-exit "^3.0.2" - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.ismatch@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" - integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash@4, lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log4js@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" - integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== - dependencies: - date-format "^3.0.0" - debug "^4.1.1" - flatted "^2.0.1" - rfdc "^1.1.4" - streamroller "^2.2.4" - -loglevel@^1.6.8: - version "1.7.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" - integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lowdb@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" - integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== - dependencies: - graceful-fs "^4.1.3" - is-promise "^2.1.0" - lodash "4" - pify "^3.0.0" - steno "^0.4.1" - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@6.0.0, lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" - integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= - dependencies: - es5-ext "~0.10.2" - -lunr-mutable-indexes@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/lunr-mutable-indexes/-/lunr-mutable-indexes-2.3.2.tgz#864253489735d598c5140f3fb75c0a5c8be2e98c" - integrity sha512-Han6cdWAPPFM7C2AigS2Ofl3XjAT0yVMrUixodJEpyg71zCtZ2yzXc3s+suc/OaNt4ca6WJBEzVnEIjxCTwFMw== - dependencies: - lunr ">= 2.3.0 < 2.4.0" - -"lunr@>= 2.3.0 < 2.4.0": - version "2.3.9" - resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" - integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== - -macos-release@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" - integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg== - -magic-string@0.25.7, magic-string@^0.25.0, magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - -magic-string@^0.22.4: - version "0.22.5" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" - integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w== - dependencies: - vlq "^0.2.2" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0, make-dir@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -make-fetch-happen@^9.0.1: - version "9.0.2" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.0.2.tgz#aa8c0e4a5e3a5f2be86c54d3abed44fe5a32ad5d" - integrity sha512-UkAWAuXPXSSlVviTjH2We20mtj1NnZW2Qq/oTY2dyMbRQ5CR3Xed3akCDMnM7j6axrMY80lhgM7loNE132PfAw== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^5.0.0" - ssri "^8.0.0" - -map-age-cleaner@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-obj@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7" - integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ== - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -marked@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7" - integrity sha512-yfCEUXmKhBPLOzEC7c+tc4XZdIeTdGoRCZakFMkCxodr7wDXqoapIME4wjcpBPJLNyUnKJ3e8rb8wlAgnLnaDw== - -marked@^2.0.1: - version "2.0.7" - resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.7.tgz#bc5b857a09071b48ce82a1f7304913a993d4b7d1" - integrity sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ== - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -mem@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" - integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== - dependencies: - map-age-cleaner "^0.1.3" - mimic-fn "^3.1.0" - -memfs@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.2.2.tgz#5de461389d596e3f23d48bb7c2afb6161f4df40e" - integrity sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q== - dependencies: - fs-monkey "1.0.3" - -memoizee@0.4.15: - version "0.4.15" - resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" - integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ== - dependencies: - d "^1.0.1" - es5-ext "^0.10.53" - es6-weak-map "^2.0.3" - event-emitter "^0.3.5" - is-promise "^2.2.2" - lru-queue "^0.1.0" - next-tick "^1.1.0" - timers-ext "^0.1.7" - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -meow@^8.0.0: - version "8.1.2" - resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" - integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-source-map@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" - integrity sha1-pd5GU42uhNQRTMXqArR3KmNGcB8= - dependencies: - source-map "^0.5.6" - -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^4.0.2: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== - dependencies: - braces "^3.0.1" - picomatch "^2.2.3" - -mime-db@1.48.0, "mime-db@>= 1.43.0 < 2": - version "1.48.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== - -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.31" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== - dependencies: - mime-db "1.48.0" - -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@2.5.2, mime@^2.4.4, mime@^2.4.5, mime@~2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-fn@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" - integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mimic-response@^2.0.0, mimic-response@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -mini-css-extract-plugin@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.0.tgz#b4db2525af2624899ed64a23b0016e0036411893" - integrity sha512-nPFKI7NSy6uONUo9yn2hIfb9vyYvkFu95qki0e21DQ9uaqNKDP15DGpK0KnV6wDroWxPHtExrdEwx/yDQ8nVRw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - webpack-sources "^1.1.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist-options@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" - -minimist@1.2.5, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" - integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-json-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" - integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== - dependencies: - jsonparse "^1.3.1" - minipass "^3.0.0" - -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== - dependencies: - yallist "^4.0.0" - -minizlib@^2.0.0, minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -modify-values@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" - integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.0.0, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -multimatch@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" - integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== - dependencies: - "@types/minimatch" "^3.0.3" - array-differ "^3.0.0" - array-union "^2.1.0" - arrify "^2.0.1" - minimatch "^3.0.4" - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -mv@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" - integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= - dependencies: - mkdirp "~0.5.1" - ncp "~2.0.0" - rimraf "~2.4.0" - -nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== - -nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -ncp@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" - integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= - -needle@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" - integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.2, negotiator@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.6.0, neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -next-tick@1, next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - -next-tick@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" - integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= - -ng-packagr@~12.0.0-next.0: - version "12.0.3" - resolved "https://registry.yarnpkg.com/ng-packagr/-/ng-packagr-12.0.3.tgz#5d8ee82a0f9f6829233423ff236bf0f550d0d30a" - integrity sha512-kVTnGedBflcZu8XA/yY99ArwqD3/fFGRA4DFwPmgmhB/80BBmZuXsRUUWXCfQDYdxb7zFkBIk9EtXDgFsi3qew== - dependencies: - "@rollup/plugin-commonjs" "^19.0.0" - "@rollup/plugin-json" "^4.1.0" - "@rollup/plugin-node-resolve" "^13.0.0" - ajv "^8.0.0" - ansi-colors "^4.1.1" - browserslist "^4.16.1" - cacache "^15.0.6" - chokidar "^3.5.1" - commander "^7.0.0" - cssnano "^5.0.0" - find-cache-dir "^3.3.1" - glob "^7.1.6" - injection-js "^2.4.0" - jsonc-parser "^3.0.0" - less "^4.1.0" - node-sass-tilde-importer "^1.0.2" - ora "^5.1.0" - postcss "^8.2.4" - postcss-preset-env "^6.7.0" - postcss-url "^10.1.1" - rollup "^2.45.1" - rollup-plugin-sourcemaps "^0.6.3" - rxjs "^6.5.0" - sass "^1.32.8" - stylus "^0.54.8" - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-fetch@2.6.1, node-fetch@^2.2.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - -node-gyp@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" - integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.3" - nopt "^5.0.0" - npmlog "^4.1.2" - request "^2.88.2" - rimraf "^3.0.2" - semver "^7.3.2" - tar "^6.0.2" - which "^2.0.2" - -node-releases@^1.1.71: - version "1.1.72" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" - integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== - -node-sass-tilde-importer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/node-sass-tilde-importer/-/node-sass-tilde-importer-1.0.2.tgz#1a15105c153f648323b4347693fdb0f331bad1ce" - integrity sha512-Swcmr38Y7uB78itQeBm3mThjxBy9/Ah/ykPIaURY/L6Nec9AyRoL/jJ7ECfMR+oZeCTVQNxVMu/aHU+TLRVbdg== - dependencies: - find-parent-dir "^0.3.0" - -node-uuid@1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0, "normalize-package-data@~1.0.1 || ^2.0.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.2.tgz#cae5c410ae2434f9a6c1baa65d5bc3b9366c8699" - integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg== - dependencies: - hosted-git-info "^4.0.1" - resolve "^1.20.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@^4.1.0, normalize-url@^4.5.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -npm-bundled@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" - integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-install-checks@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" - integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== - dependencies: - semver "^7.1.1" - -npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-package-arg@8.1.4, npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2: - version "8.1.4" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.4.tgz#8001cdbc4363997b8ef6c6cf7aaf543c5805879d" - integrity sha512-xLokoCFqj/rPdr3LvcdDL6Kj6ipXGEDHD/QGpzwU6/pibYUOXmp5DBmg76yukFyx4ZDbrXNOTn+BPyd8TD4Jlw== - dependencies: - hosted-git-info "^4.0.1" - semver "^7.3.4" - validate-npm-package-name "^3.0.0" - -"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0": - version "6.1.1" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" - integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== - dependencies: - hosted-git-info "^2.7.1" - osenv "^0.1.5" - semver "^5.6.0" - validate-npm-package-name "^3.0.0" - -npm-packlist@^2.1.4: - version "2.2.2" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" - integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== - dependencies: - glob "^7.1.6" - ignore-walk "^3.0.3" - npm-bundled "^1.1.1" - npm-normalize-package-bin "^1.0.1" - -npm-pick-manifest@6.1.1, npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" - integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== - dependencies: - npm-install-checks "^4.0.0" - npm-normalize-package-bin "^1.0.1" - npm-package-arg "^8.1.2" - semver "^7.3.4" - -npm-registry-client@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.6.0.tgz#7f1529f91450732e89f8518e0f21459deea3e4c4" - integrity sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg== - dependencies: - concat-stream "^1.5.2" - graceful-fs "^4.1.6" - normalize-package-data "~1.0.1 || ^2.0.0" - npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - once "^1.3.3" - request "^2.74.0" - retry "^0.10.0" - safe-buffer "^5.1.1" - semver "2 >=2.2.1 || 3.x || 4 || 5" - slide "^1.1.3" - ssri "^5.2.4" - optionalDependencies: - npmlog "2 || ^3.1.0 || ^4.0.0" - -npm-registry-fetch@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" - integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== - dependencies: - make-fetch-happen "^9.0.1" - minipass "^3.1.3" - minipass-fetch "^1.3.0" - minipass-json-stream "^1.0.1" - minizlib "^2.0.0" - npm-package-arg "^8.0.0" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -"npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== - dependencies: - boolbase "^1.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -nwsapi@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" - integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== - -object-inspect@~1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.4.1.tgz#37ffb10e71adaf3748d05f713b4c9452f402cbc4" - integrity sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw== - -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -object.values@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" - integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/open/-/open-8.2.0.tgz#d6a4788b00009a9d60df471ecb89842a15fdcfc1" - integrity sha512-O8uInONB4asyY3qUcEytpgwxQG3O0fJ/hlssoUHsBboOIRVZzT6Wq+Rwj5nffbeUhOdMjpXeISpDDzHCMRDuOQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -ora@5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - dependencies: - bl "^4.1.0" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - is-unicode-supported "^0.1.0" - log-symbols "^4.1.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -ora@^5.0.0, ora@^5.1.0, ora@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.0.tgz#42eda4855835b9cd14d33864c97a3c95a3f56bf4" - integrity sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg== - dependencies: - bl "^4.1.0" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - is-unicode-supported "^0.1.0" - log-symbols "^4.1.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-name@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" - integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== - dependencies: - macos-release "^2.2.0" - windows-release "^3.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4, osenv@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-cancelable@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" - integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -p-event@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" - integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ== - dependencies: - p-timeout "^3.1.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-timeout@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" - integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== - dependencies: - p-finally "^1.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pacote@11.3.4: - version "11.3.4" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.4.tgz#c290b790a5cee3082bb8fa223f3f3e2fdf3d0bfc" - integrity sha512-RfahPCunM9GI7ryJV/zY0bWQiokZyLqaSNHXtbNSoLb7bwTvBbJBEyCJ01KWs4j1Gj7GmX8crYXQ1sNX6P2VKA== - dependencies: - "@npmcli/git" "^2.0.1" - "@npmcli/installed-package-contents" "^1.0.6" - "@npmcli/promise-spawn" "^1.2.0" - "@npmcli/run-script" "^1.8.2" - cacache "^15.0.5" - chownr "^2.0.0" - fs-minipass "^2.1.0" - infer-owner "^1.0.4" - minipass "^3.1.3" - mkdirp "^1.0.3" - npm-package-arg "^8.0.1" - npm-packlist "^2.1.4" - npm-pick-manifest "^6.0.0" - npm-registry-fetch "^11.0.0" - promise-retry "^2.0.1" - read-package-json-fast "^2.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.1.0" - -pako@^0.2.5: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= - -pako@^1.0.6, pako@~1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-ms@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" - integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== - -parse-node-version@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -parse5-html-rewriting-stream@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz#de1820559317ab4e451ea72dba05fddfd914480b" - integrity sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg== - dependencies: - parse5 "^6.0.1" - parse5-sax-parser "^6.0.1" - -parse5-htmlparser2-tree-adapter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - -parse5-sax-parser@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz#98b4d366b5b266a7cd90b4b58906667af882daba" - integrity sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg== - dependencies: - parse5 "^6.0.1" - -parse5@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" - integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== - -parse5@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1, path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -pidtree@0.5.0, pidtree@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.5.0.tgz#ad5fbc1de78b8a5f99d6fbdd4f6e4eee21d1aca1" - integrity sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA== - -pidusage@2.0.21, pidusage@^2.0.17: - version "2.0.21" - resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-2.0.21.tgz#7068967b3d952baea73e57668c98b9eaa876894e" - integrity sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA== - dependencies: - safe-buffer "^5.2.1" - -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pino-std-serializers@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" - integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== - -pino@6.11.3: - version "6.11.3" - resolved "https://registry.yarnpkg.com/pino/-/pino-6.11.3.tgz#0c02eec6029d25e6794fdb6bbea367247d74bc29" - integrity sha512-drPtqkkSf0ufx2gaea3TryFiBHdNIdXKf5LN0hTM82SXI4xVIve2wLwNg92e1MT6m3jASLu6VO7eGY6+mmGeyw== - dependencies: - fast-redact "^3.0.0" - fast-safe-stringify "^2.0.7" - flatstr "^1.0.12" - pino-std-serializers "^3.1.0" - quick-format-unescaped "^4.0.3" - sonic-boom "^1.0.2" - -pkg-dir@4.2.0, pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkginfo@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" - integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= - -pluralize@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" - integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== - -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - -popper.js@^1.14.1: - version "1.16.1" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" - integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== - -portfinder@^1.0.26: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" - integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^6.0.2" - -postcss-calc@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" - integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== - dependencies: - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" - -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-gray@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-color-hex-alpha@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" - integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== - dependencies: - postcss "^7.0.14" - postcss-values-parser "^2.0.1" - -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-colormin@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88" - integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw== - dependencies: - browserslist "^4.16.6" - caniuse-api "^3.0.0" - colord "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-convert-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232" - integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" - -postcss-custom-properties@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" - integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== - dependencies: - postcss "^7.0.17" - postcss-values-parser "^2.0.1" - -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-dir-pseudo-class@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-discard-comments@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" - integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== - -postcss-discard-duplicates@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d" - integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== - -postcss-discard-empty@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" - integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== - -postcss-discard-overridden@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" - integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== - -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== - dependencies: - postcss "^7.0.2" - -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== - dependencies: - postcss "^7.0.2" - -postcss-font-variant@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" - integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== - dependencies: - postcss "^7.0.2" - -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" - -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-import@14.0.2: - version "14.0.2" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.0.2.tgz#60eff77e6be92e7b67fe469ec797d9424cae1aa1" - integrity sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-initial@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" - integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== - dependencies: - postcss "^7.0.2" - -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-loader@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-5.3.0.tgz#1657f869e48d4fdb018a40771c235e499ee26244" - integrity sha512-/+Z1RAmssdiSLgIZwnJHwBMnlABPgF7giYzTN2NOfr9D21IJZ4mQC1R2miwp80zno9M4zMD/umGI8cR+2EL5zw== - dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.4" - semver "^7.3.4" - -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== - dependencies: - postcss "^7.0.2" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-merge-longhand@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41" - integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw== - dependencies: - css-color-names "^1.0.1" - postcss-value-parser "^4.1.0" - stylehacks "^5.0.1" - -postcss-merge-rules@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a" - integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg== - dependencies: - browserslist "^4.16.6" - caniuse-api "^3.0.0" - cssnano-utils "^2.0.1" - postcss-selector-parser "^6.0.5" - vendors "^1.0.3" - -postcss-minify-font-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" - integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-minify-gradients@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz#2dc79fd1a1afcb72a9e727bc549ce860f93565d2" - integrity sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g== - dependencies: - cssnano-utils "^2.0.1" - is-color-stop "^1.1.0" - postcss-value-parser "^4.1.0" - -postcss-minify-params@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c" - integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw== - dependencies: - alphanum-sort "^1.0.2" - browserslist "^4.16.0" - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - uniqs "^2.0.0" - -postcss-minify-selectors@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" - integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== - dependencies: - alphanum-sort "^1.0.2" - postcss-selector-parser "^6.0.5" - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-nesting@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" - integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== - dependencies: - postcss "^7.0.2" - -postcss-normalize-charset@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" - integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== - -postcss-normalize-display-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" - integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-positions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" - integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-normalize-repeat-style@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" - integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-string@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" - integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-normalize-timing-functions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" - integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-normalize-unicode@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" - integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== - dependencies: - browserslist "^4.16.0" - postcss-value-parser "^4.1.0" - -postcss-normalize-url@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.1.tgz#ffa9fe545935d8b57becbbb7934dd5e245513183" - integrity sha512-hkbG0j58Z1M830/CJ73VsP7gvlG1yF+4y7Fd1w4tD2c7CaA2Psll+pQ6eQhth9y9EaqZSLzamff/D0MZBMbYSg== - dependencies: - is-absolute-url "^3.0.3" - normalize-url "^4.5.0" - postcss-value-parser "^4.1.0" - -postcss-normalize-whitespace@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" - integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== - dependencies: - postcss-value-parser "^4.1.0" - -postcss-ordered-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.1.tgz#79ef6e2bd267ccad3fc0c4f4a586dfd01c131f64" - integrity sha512-6mkCF5BQ25HvEcDfrMHCLLFHlraBSlOXFnQMHYhSpDO/5jSR1k8LdEXOkv+7+uzW6o6tBYea1Km0wQSRkPJkwA== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== - dependencies: - postcss "^7.0.2" - -postcss-page-break@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" - -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-preset-env@6.7.0, postcss-preset-env@^6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" - integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== - dependencies: - autoprefixer "^9.6.1" - browserslist "^4.6.4" - caniuse-lite "^1.0.30000981" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.4.0" - postcss "^7.0.17" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.3" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.8" - postcss-custom-properties "^8.0.11" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-reduce-initial@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" - integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== - dependencies: - browserslist "^4.16.0" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" - integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== - dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== - dependencies: - postcss "^7.0.2" - -postcss-selector-matches@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-not@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" - integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== - dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: - version "6.0.6" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" - integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-svgo@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f" - integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A== - dependencies: - postcss-value-parser "^4.1.0" - svgo "^2.3.0" - -postcss-unique-selectors@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc" - integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w== - dependencies: - alphanum-sort "^1.0.2" - postcss-selector-parser "^6.0.5" - uniqs "^2.0.0" - -postcss-url@^10.1.1: - version "10.1.3" - resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-10.1.3.tgz#54120cc910309e2475ec05c2cfa8f8a2deafdf1e" - integrity sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw== - dependencies: - make-dir "~3.1.0" - mime "~2.5.2" - minimatch "~3.0.4" - xxhashjs "~0.2.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss@7.x.x, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@8.3.0, postcss@^8.2.15, postcss@^8.2.4, postcss@^8.2.9: - version "8.3.0" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f" - integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ== - dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map-js "^0.6.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prettier-bytes@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6" - integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY= - -prettier@^2.0.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" - integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== - -pretty-bytes@^5.3.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - -pretty-ms@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.1.0.tgz#b906bdd1ec9e9799995c372e2b1c34f073f95384" - integrity sha512-4gaK1skD2gwscCfkswYQRmddUb2GJZtzDGRjHWadVHtK/DIKFufa12MvES6/xu1tVbUYeia5bmLcwJtZJQUqnw== - dependencies: - parse-ms "^2.1.0" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -progress@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" - integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== - -progress@2.0.3, progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -protobufjs@6.8.8: - version "6.8.8" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" - integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.0" - "@types/node" "^10.1.0" - long "^4.0.0" - -protractor@^7.0.0, protractor@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/protractor/-/protractor-7.0.0.tgz#c3e263608bd72e2c2dc802b11a772711a4792d03" - integrity sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw== - dependencies: - "@types/q" "^0.0.32" - "@types/selenium-webdriver" "^3.0.0" - blocking-proxy "^1.0.0" - browserstack "^1.5.1" - chalk "^1.1.3" - glob "^7.0.3" - jasmine "2.8.0" - jasminewd2 "^2.1.0" - q "1.4.1" - saucelabs "^1.5.0" - selenium-webdriver "3.6.0" - source-map-support "~0.4.0" - webdriver-js-extender "2.1.0" - webdriver-manager "^12.1.7" - yargs "^15.3.1" - -proxy-addr@~2.0.5: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -psl@^1.1.24, psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -puppeteer@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-10.0.0.tgz#1b597c956103e2d989ca17f41ba4693b20a3640c" - integrity sha512-AxHvCb9IWmmP3gMW+epxdj92Gglii+6Z4sb+W+zc2hTTu10HF0yg6hGXot5O74uYkVqG3lfDRLfnRpi6WOwi5A== - dependencies: - debug "4.3.1" - devtools-protocol "0.0.883894" - extract-zip "2.0.1" - https-proxy-agent "5.0.0" - node-fetch "2.6.1" - pkg-dir "4.2.0" - progress "2.0.1" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.0.0" - unbzip2-stream "1.3.3" - ws "7.4.6" - -q@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4= - -q@^1.4.1, q@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qjobs@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" - integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-format-unescaped@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz#6d6b66b8207aa2b35eef12be1421bb24c428f652" - integrity sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg== - -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -quicktype-core@6.0.69: - version "6.0.69" - resolved "https://registry.yarnpkg.com/quicktype-core/-/quicktype-core-6.0.69.tgz#955347b64e8a7b6a37af49fe12f5772abc153b8e" - integrity sha512-wKQ+/fwgdtFOcbeRiZkIBLA2ajvrFvmtTmexdv7PlO1dyp3C7Irbn2/HjwzalD1dYFrtMEYWohB/4rr3Mg75Xw== - dependencies: - "@mark.probst/unicode-properties" "~1.1.0" - browser-or-node "^1.2.1" - collection-utils "^1.0.1" - is-url "^1.2.4" - isomorphic-fetch "^2.2.1" - js-base64 "^2.4.3" - pako "^1.0.6" - pluralize "^7.0.0" - readable-stream "2.3.0" - urijs "^1.19.1" - wordwrap "^1.0.0" - yaml "^1.5.0" - -quote-stream@^1.0.1, quote-stream@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2" - integrity sha1-hJY/jJwmuULhU/7rU6rnRlK34LI= - dependencies: - buffer-equal "0.0.1" - minimist "^1.1.3" - through2 "^2.0.0" - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -raw-loader@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" - integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= - dependencies: - pify "^2.3.0" - -read-installed@~4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" - integrity sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc= - dependencies: - debuglog "^1.0.1" - read-package-json "^2.0.0" - readdir-scoped-modules "^1.0.0" - semver "2 || 3 || 4 || 5" - slide "~1.1.3" - util-extend "^1.0.1" - optionalDependencies: - graceful-fs "^4.1.2" - -read-package-json-fast@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.2.tgz#2dcb24d9e8dd50fb322042c8c35a954e6cc7ac9e" - integrity sha512-5fyFUyO9B799foVk4n6ylcoAktG/FbE3jwRKxvwaeSrIunaoMc0u81dzXxjeAFKOce7O5KncdfwpGvvs6r5PsQ== - dependencies: - json-parse-even-better-errors "^2.3.0" - npm-normalize-package-bin "^1.0.1" - -read-package-json@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" - integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== - dependencies: - glob "^7.1.1" - json-parse-even-better-errors "^2.3.0" - normalize-package-data "^2.0.0" - npm-normalize-package-bin "^1.0.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -readable-stream@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.0.tgz#640f5dcda88c91a8dc60787145629170813a1ed2" - integrity sha512-c7KMXGd4b48nN3OJ1U9qOsn6pXNzf6kLd3kdZCkg2sxAcoiufInqF0XckwEnlrcwuaYwonlNK8GQUIOC/WC7sg== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.1.0" - string_decoder "~1.0.0" - util-deprecate "~1.0.1" - -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readdir-scoped-modules@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" - integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== - dependencies: - debuglog "^1.0.1" - dezalgo "^1.0.0" - graceful-fs "^4.1.2" - once "^1.3.0" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -reflect-metadata@^0.1.13, reflect-metadata@^0.1.2: - version "0.1.13" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== - -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@0.13.7, regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== - -regexp.prototype.flags@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" - integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -regjsgen@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== - -regjsparser@^0.6.4: - version "0.6.9" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6" - integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ== - dependencies: - jsesc "~0.5.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise-native@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -request@2.88.2, request@^2.74.0, request@^2.87.0, request@^2.88.0, request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url-loader@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" - integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== - dependencies: - adjust-sourcemap-loader "^4.0.0" - convert-source-map "^1.7.0" - loader-utils "^2.0.0" - postcss "^7.0.35" - source-map "0.6.1" - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.20.0, resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -responselike@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" - integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== - dependencies: - lowercase-keys "^2.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.1.4: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@~2.4.0: - version "2.4.5" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" - integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= - dependencies: - glob "^6.0.1" - -rimraf@~2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rollup-plugin-sourcemaps@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" - integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== - dependencies: - "@rollup/pluginutils" "^3.0.9" - source-map-resolve "^0.6.0" - -rollup@^2.45.1: - version "2.51.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.51.0.tgz#ffd847882283998fc8611cd57af917f173b4ab5c" - integrity sha512-ITLt9sScNCBVspSHauw/W49lEZ0vjN8LyCzSNsNaqT67vTss2lYEfOyxltX8hjrhr1l/rQwmZ2wazzEqhZ/fUg== - optionalDependencies: - fsevents "~2.3.1" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@6.6.7, rxjs@^6.4.0, rxjs@^6.5.0, rxjs@^6.5.3, rxjs@^6.6.6: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-loader@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.0.0.tgz#ba91df5725cb9676c8e0937002a647ab011eb94a" - integrity sha512-LJQMyDdNdhcvoO2gJFw7KpTaioVFDeRJOuatRDUNgCIqyu4s4kgDsNofdGzAZB1zFOgo/p3fy+aR/uGXamcJBg== - dependencies: - klona "^2.0.4" - neo-async "^2.6.2" - -sass@1.34.1, sass@^1.32.8: - version "1.34.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.34.1.tgz#30f45c606c483d47b634f1e7371e13ff773c96ef" - integrity sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ== - dependencies: - chokidar ">=3.0.0 <4.0.0" - -"sauce-connect-proxy@https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz": - version "0.0.0" - resolved "https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz#992e2cb0d91e54b27a4f5bbd2049f3b774718115" - -saucelabs@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" - integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== - dependencies: - https-proxy-agent "^2.2.1" - -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -saxes@^3.1.9: - version "3.1.11" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" - integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== - dependencies: - xmlchars "^2.1.1" - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^2.6.5, schema-utils@^2.7.0: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== - dependencies: - "@types/json-schema" "^7.0.6" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -seedrandom@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" - integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selenium-webdriver@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.5.0.tgz#9036c82874e6c0f5cbff0a0f18223bc31c99cb77" - integrity sha512-1bCZYRfDy7vsu1dkLrclTLvWPxSo6rOIkxZXvB2wnzeWkEoiTKpw612EUGA3jRZxPzAzI9OlxuULJV8ge1vVXQ== - dependencies: - jszip "^3.1.3" - rimraf "^2.5.4" - tmp "0.0.30" - xml2js "^0.4.17" - -selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" - integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== - dependencies: - jszip "^3.1.3" - rimraf "^2.5.4" - tmp "0.0.30" - xml2js "^0.4.17" - -selfsigned@^1.10.8: - version "1.10.11" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9" - integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA== - dependencies: - node-forge "^0.10.0" - -semver-dsl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/semver-dsl/-/semver-dsl-1.0.1.tgz#d3678de5555e8a61f629eed025366ae5f27340a0" - integrity sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA= - dependencies: - semver "^5.3.0" - -"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@7.3.5, semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-immediate-shim@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallow-copy@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" - integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA= - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shelljs@^0.8.3, shelljs@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slide@^1.1.3, slide@~1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= - -smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -socket.io-adapter@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527" - integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== - -socket.io-parser@~4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" - integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== - dependencies: - "@types/component-emitter" "^1.2.10" - component-emitter "~1.3.0" - debug "~4.3.1" - -socket.io@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a" - integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw== - dependencies: - "@types/cookie" "^0.4.0" - "@types/cors" "^2.8.8" - "@types/node" ">=10.0.0" - accepts "~1.3.4" - base64id "~2.0.0" - debug "~4.3.1" - engine.io "~4.1.0" - socket.io-adapter "~2.1.0" - socket.io-parser "~4.0.3" - -sockjs-client@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6" - integrity sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ== - dependencies: - debug "^3.2.6" - eventsource "^1.0.7" - faye-websocket "^0.11.3" - inherits "^2.0.4" - json3 "^3.3.3" - url-parse "^1.5.1" - -sockjs@^0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" - integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== - dependencies: - faye-websocket "^0.11.3" - uuid "^3.4.0" - websocket-driver "^0.7.4" - -socks-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" - integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== - dependencies: - agent-base "6" - debug "4" - socks "^2.3.3" - -socks@^2.3.3: - version "2.6.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" - integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== - dependencies: - ip "^1.1.5" - smart-buffer "^4.1.0" - -sonic-boom@^1.0.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" - integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== - dependencies: - atomic-sleep "^1.0.0" - flatstr "^1.0.12" - -source-list-map@^2.0.0, source-list-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== - -source-map-loader@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049" - integrity sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw== - dependencies: - abab "^2.0.5" - iconv-lite "^0.6.2" - source-map-js "^0.6.2" - -source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - -source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@0.5.9: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.4.0: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@0.7.3, source-map@^0.7.3, source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -spdx-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7" - integrity sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A== - dependencies: - array-find-index "^1.0.2" - spdx-expression-parse "^3.0.0" - spdx-ranges "^2.0.0" - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" - integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== - -spdx-ranges@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/spdx-ranges/-/spdx-ranges-2.1.1.tgz#87573927ba51e92b3f4550ab60bfc83dd07bac20" - integrity sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA== - -spdx-satisfies@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz#9a09a68d80f5f1a31cfaebb384b0c6009e4969fe" - integrity sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA== - dependencies: - spdx-compare "^1.0.0" - spdx-expression-parse "^3.0.0" - spdx-ranges "^2.0.0" - -spdx-satisfies@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz#9feeb2524686c08e5f7933c16248d4fdf07ed6a6" - integrity sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw== - dependencies: - spdx-compare "^1.0.0" - spdx-expression-parse "^3.0.0" - spdx-ranges "^2.0.0" - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -split2@^3.0.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" - -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -ssri@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" - integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== - dependencies: - safe-buffer "^5.1.1" - -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -static-eval@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.1.0.tgz#a16dbe54522d7fa5ef1389129d813fd47b148014" - integrity sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw== - dependencies: - escodegen "^1.11.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -static-module@^2.2.0: - version "2.2.5" - resolved "https://registry.yarnpkg.com/static-module/-/static-module-2.2.5.tgz#bd40abceae33da6b7afb84a0e4329ff8852bfbbf" - integrity sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ== - dependencies: - concat-stream "~1.6.0" - convert-source-map "^1.5.1" - duplexer2 "~0.1.4" - escodegen "~1.9.0" - falafel "^2.1.0" - has "^1.0.1" - magic-string "^0.22.4" - merge-source-map "1.0.4" - object-inspect "~1.4.0" - quote-stream "~1.0.2" - readable-stream "~2.3.3" - shallow-copy "~0.0.1" - static-eval "^2.0.0" - through2 "~2.0.3" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - -steno@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" - integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs= - dependencies: - graceful-fs "^4.1.3" - -streamroller@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53" - integrity sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ== - dependencies: - date-format "^2.1.0" - debug "^4.1.1" - fs-extra "^8.1.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -style-loader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" - integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -stylehacks@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb" - integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== - dependencies: - browserslist "^4.16.0" - postcss-selector-parser "^6.0.4" - -stylus-loader@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-6.0.0.tgz#ea3c4adb4cee492ebf6d63dea2e1743480ca29a4" - integrity sha512-cB3Mtu2+JGZYm5WUzGyo4En6r087F11eaZK+EUn/6Fj8Eo4mY5DpWH82lvTfjAV2EP42Fd7NWvWG20sJYCyuFw== - dependencies: - fast-glob "^3.2.5" - klona "^2.0.4" - normalize-path "^3.0.0" - -stylus@0.54.8, stylus@^0.54.8: - version "0.54.8" - resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" - integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== - dependencies: - css-parse "~2.0.0" - debug "~3.1.0" - glob "^7.1.6" - mkdirp "~1.0.4" - safer-buffer "^2.1.2" - sax "~1.2.4" - semver "^6.3.0" - source-map "^0.7.3" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -svgo@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.0.tgz#6b3af81d0cbd1e19c83f5f63cec2cb98c70b5373" - integrity sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q== - dependencies: - "@trysound/sax" "0.1.1" - chalk "^4.1.0" - commander "^7.1.0" - css-select "^3.1.2" - css-tree "^1.1.2" - csso "^4.2.0" - stable "^0.1.8" - -symbol-observable@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" - integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== - -symbol-tree@^3.2.2: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - -table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== - dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" - integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== - -tar-fs@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" - integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== - dependencies: - chownr "^1.1.1" - mkdirp "^0.5.1" - pump "^3.0.0" - tar-stream "^2.0.0" - -tar-stream@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.0.0, tar@^6.0.2, tar@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" - integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -temp@^0.9.0: - version "0.9.4" - resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.4.tgz#cd20a8580cb63635d0e4e9d4bd989d44286e7620" - integrity sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA== - dependencies: - mkdirp "^0.5.1" - rimraf "~2.6.2" - -terser-webpack-plugin@5.1.3, terser-webpack-plugin@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz#30033e955ca28b55664f1e4b30a1347e61aa23af" - integrity sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A== - dependencies: - jest-worker "^27.0.2" - p-limit "^3.1.0" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - source-map "^0.6.1" - terser "^5.7.0" - -terser@5.7.0, terser@^5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" - integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -text-extensions@^1.0.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" - integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== - -text-table@0.2.0, text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through2@^2.0.0, through2@~2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through2@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" - integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== - dependencies: - readable-stream "3" - -through@2, "through@>=2.2.7 <3", through@X.X.X, through@^2.3.6, through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timers-ext@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" - integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== - dependencies: - es5-ext "~0.10.46" - next-tick "1" - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - -tiny-inflate@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" - integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== - -tmp@0.0.30: - version "0.0.30" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" - integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= - dependencies: - os-tmpdir "~1.0.1" - -tmp@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" - integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -tree-kill@1.2.2, tree-kill@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== - -treeify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" - integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== - -trim-off-newlines@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" - integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= - -ts-api-guardian@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/ts-api-guardian/-/ts-api-guardian-0.6.0.tgz#9f44cf9bad1db5de358ccca7b4e6fb1d2c7fe462" - integrity sha512-DVA+UgPI1TVRI7GNDG1XBxwGs+cKqJq1os00txr52TUxBPwsUWWT7f83VHWvPQvh7G2wj93U/dTUBeDq3FIDTg== - dependencies: - chalk "^2.3.1" - diff "^3.5.0" - minimist "^1.2.0" - -ts-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be" - integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg== - dependencies: - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.17" - yn "3.1.1" - -tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" - strip-bom "^3.0.0" - -tslib@2.2.0, tslib@^2.0.0, tslib@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== - -tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslint@^6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" - integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.3" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.13.0" - tsutils "^2.29.0" - -tsscmp@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" - integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== - -tsutils@2.27.2: - version "2.27.2" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7" - integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg== - dependencies: - tslib "^1.8.1" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -typanion@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.3.1.tgz#d1ab4930d7b0d165d4356405891693b5648e3bf1" - integrity sha512-VogBiMj3ZQuWaHkbhXwSgd9jXE4s7EMaaV7VSEiKTNYnKJs/bPjvcOGbD7rTM9aPqTABvgLVEZ+iFP6ab12HtQ== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" - integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== - -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.0.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d" - integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw== - -typed-graphqlify@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/typed-graphqlify/-/typed-graphqlify-3.1.2.tgz#4bd26c11082b130bebc811030f892e2b22902d66" - integrity sha512-z0x63rRNswSKbIIjZyooKMzV4tHLt1OhWmsUkz1dohnv8+6T1J55DCRRb1CFaYWmkD8DkJ9miSP8VNB9N/1VPQ== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typescript@4.2.4, typescript@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== - -ua-parser-js@^0.7.23: - version "0.7.28" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" - integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== - -uglify-js@^3.1.4: - version "3.13.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.9.tgz#4d8d21dcd497f29cfd8e9378b9df123ad025999b" - integrity sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -unbzip2-stream@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" - integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== - -unicode-trie@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-0.3.1.tgz#d671dddd89101a08bac37b6a5161010602052085" - integrity sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU= - dependencies: - pako "^0.2.5" - tiny-inflate "^1.0.0" - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - -universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -unix-crypt-td-js@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz#4912dfad1c8aeb7d20fa0a39e4c31918c1d5d5dd" - integrity sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -urijs@^1.19.1: - version "1.19.6" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.6.tgz#51f8cb17ca16faefb20b9a31ac60f84aa2b7c870" - integrity sha512-eSXsXZ2jLvGWeLYlQA3Gh36BcjF+0amo92+wHPyN1mdR8Nxf75fuEuYTd9c0a+m/vhCjRK0ESlE9YNLW+E1VEw== - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse@^1.4.3, url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util-extend@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" - integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -v8-to-istanbul@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" - integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" - integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= - dependencies: - builtins "^1.0.3" - -validator@13.6.0: - version "13.6.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" - integrity sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vendors@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - -verdaccio-audit@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/verdaccio-audit/-/verdaccio-audit-10.0.0.tgz#d3304d923c7f2c28c173a02425208c941f25217b" - integrity sha512-Epsh+C7ZEdq39PR9QeDBTWktbeqc0zOQjMzWte6Ut5Jh6fPLZzxGF8VK8O67B6mnTwLvGy50A1aPVM97Ysh5Rw== - dependencies: - express "4.17.1" - request "2.88.2" - -verdaccio-auth-memory@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/verdaccio-auth-memory/-/verdaccio-auth-memory-10.0.0.tgz#057f7f94e96d21c69b1ba7983c00fe8cb7072fef" - integrity sha512-pl6WDwYUNetGvFT6Veh3LuNpWLqxuDBC3i1URvuI+biIfAeqgr2M07ICN9p1cNlyWKdcEC/YILqCYg9aE/KwsQ== - dependencies: - "@verdaccio/commons-api" "^10.0.0" - -verdaccio-htpasswd@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/verdaccio-htpasswd/-/verdaccio-htpasswd-10.0.0.tgz#7a7f44e8ed4db40c53deef0f5101f2a16dce4ff1" - integrity sha512-3TKwiLwl8/fbaTDawHvjSYcsyMmdARg58keP/1plv74x+Jw0sC66HbbRwQ/tPO5mqoG0UwoWW+lkO8h/OiWi9w== - dependencies: - "@verdaccio/file-locking" "^10.0.0" - apache-md5 "1.1.2" - bcryptjs "2.4.3" - http-errors "1.8.0" - unix-crypt-td-js "1.1.4" - -verdaccio@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/verdaccio/-/verdaccio-5.1.0.tgz#541d0cefe64c8ae3fed268611e8063815572c0fa" - integrity sha512-NydMLcy4rjnrZdKWtlCspahCIKOUlTRwF484EeweVYAwyh/5K5ph8E8lteDPnOD1UxfRZ+tx/BkQfqHp1udgXg== - dependencies: - "@verdaccio/commons-api" "10.0.0" - "@verdaccio/local-storage" "10.0.6" - "@verdaccio/readme" "10.0.0" - "@verdaccio/streams" "10.0.0" - "@verdaccio/ui-theme" "3.1.0" - JSONStream "1.3.5" - async "3.2.0" - body-parser "1.19.0" - clipanion "3.0.0-rc.12" - compression "1.7.4" - cookies "0.8.0" - cors "2.8.5" - dayjs "1.10.4" - debug "^4.3.1" - envinfo "7.8.1" - eslint-import-resolver-node "0.3.4" - express "4.17.1" - fast-safe-stringify "^2.0.7" - handlebars "4.7.7" - http-errors "1.8.0" - js-yaml "4.1.0" - jsonwebtoken "8.5.1" - kleur "4.1.4" - lodash "4.17.21" - lru-cache "6.0.0" - lunr-mutable-indexes "2.3.2" - marked "2.0.5" - memoizee "0.4.15" - mime "2.5.2" - minimatch "3.0.4" - mkdirp "1.0.4" - mv "2.1.1" - pino "6.11.3" - pkginfo "0.4.1" - prettier-bytes "^1.0.4" - pretty-ms "^5.0.0" - request "2.88.0" - semver "7.3.5" - validator "13.6.0" - verdaccio-audit "10.0.0" - verdaccio-htpasswd "10.0.0" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vlq@^0.2.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" - integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== - -void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" - integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= - -w3c-hr-time@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -w3c-xmlserializer@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" - integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== - dependencies: - domexception "^1.0.1" - webidl-conversions "^4.0.2" - xml-name-validator "^3.0.0" - -watchpack@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce" - integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= - dependencies: - defaults "^1.0.3" - -webdriver-js-extender@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" - integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== - dependencies: - "@types/selenium-webdriver" "^3.0.0" - selenium-webdriver "^3.0.1" - -webdriver-manager@^12.1.7: - version "12.1.8" - resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.8.tgz#5e70e73eaaf53a0767d5745270addafbc5905fd4" - integrity sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg== - dependencies: - adm-zip "^0.4.9" - chalk "^1.1.1" - del "^2.2.0" - glob "^7.0.3" - ini "^1.3.4" - minimist "^1.2.0" - q "^1.4.1" - request "^2.87.0" - rimraf "^2.5.2" - semver "^5.3.0" - xml2js "^0.4.17" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -webpack-dev-middleware@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz#0abe825275720e0a339978aea5f0b03b140c1584" - integrity sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw== - dependencies: - colorette "^1.2.2" - mem "^8.1.1" - memfs "^3.2.2" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^3.0.0" - -webpack-dev-middleware@^3.7.2: - version "3.7.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" - integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@3.11.2: - version "3.11.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" - integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.3.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.8" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.26" - schema-utils "^1.0.0" - selfsigned "^1.10.8" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "^0.3.21" - sockjs-client "^1.5.0" - spdy "^4.0.2" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-merge@5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-sources@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.0.tgz#9ed2de69b25143a4c18847586ad9eccb19278cfa" - integrity sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - -webpack-subresource-integrity@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz#e40b6578d3072e2d24104975249c52c66e9a743e" - integrity sha512-GBWYBoyalbo5YClwWop9qe6Zclp8CIXYGIz12OPclJhIrSplDxs1Ls1JDMH8xBPPrg1T6ISaTW9Y6zOrwEiAzw== - dependencies: - webpack-sources "^1.3.0" - -webpack@5.38.1, webpack@^5.1.0: - version "5.38.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.38.1.tgz#5224c7f24c18e729268d3e3bc97240d6e880258e" - integrity sha512-OqRmYD1OJbHZph6RUMD93GcCZy4Z4wC0ele4FXyYF0J6AxO1vOSuIlU1hkS/lDlR9CDYBz64MZRmdbdnFFoT2g== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.47" - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/wasm-edit" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - acorn "^8.2.1" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.0" - es-module-lexer "^0.4.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.0.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.1" - watchpack "^2.2.0" - webpack-sources "^2.3.0" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-fetch@>=0.10.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" - integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== - -whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.1, which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -windows-release@^3.1.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999" - integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg== - dependencies: - execa "^1.0.0" - -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -ws@7.4.6, ws@^7.0.0, ws@~7.4.2: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -ws@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== - dependencies: - async-limiter "~1.0.0" - -xhr2@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.2.1.tgz#4e73adc4f9cfec9cbd2157f73efdce3a5f108a93" - integrity sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw== - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -xml2js@^0.4.17: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -xmlchars@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== - -xmldom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" - integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA== - -xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xxhashjs@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" - integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== - dependencies: - cuint "^0.2.2" - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0, yaml@^1.5.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^20.0.0, yargs-parser@^20.2.2, yargs-parser@^20.2.3: - version "20.2.7" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - -yargs@^16.0.0, yargs@^16.1.1, yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.0.0: - version "17.0.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" - integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zone.js@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.4.tgz#0f70dcf6aba80f698af5735cbb257969396e8025" - integrity sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw== - dependencies: - tslib "^2.0.0" - -zone.js@~0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" - integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==