diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 7b237d6d1cf7..000000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,25 +0,0 @@ -environment: - # Note: if updating to Node 10, use at least 10.5.0 to include a fix for - # https://github.com/nodejs/node/issues/20297 - nodejs_version: "8.9.2" # Same version as used in CircleCI. - -matrix: - fast_finish: true - -install: - - ps: Install-Product node $env:nodejs_version - - yarn install --frozen-lockfile - - npm run webdriver-update-appveyor - -test_script: - - node --version - - yarn --version - - yarn test - - node tests\legacy-cli\run_e2e.js --appveyor "--glob=tests/{basic,commands,generate,build/styles}/**" - -build: off -deploy: off - -cache: - - node_modules -> yarn.lock - - "%LOCALAPPDATA%\\Yarn" diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000000..de4d1f007dd1 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000000..776a8225c3e7 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,62 @@ +# Make TypeScript compilation fast, by keeping a few copies of the compiler +# running as daemons, and cache SourceFile AST's to reduce parse time. +build --strategy=TypeScriptCompile=worker + +# Performance: avoid stat'ing input files +build --watchfs + +test --test_output=errors + +################################ +# Remote Execution Setup # +################################ + + # Use the Angular team internal GCP instance for remote execution. +build:remote --remote_instance_name=projects/internal-200822/instances/default_instance +build:remote --project_id=internal-200822 + + # Setup the build strategy for various types of actions. Mixing "local" and "remote" +# can cause unexpected results and we want to run everything remotely if possible. +build:remote --spawn_strategy=remote +build:remote --strategy=Javac=remote +build:remote --strategy=Closure=remote +build:remote --strategy=Genrule=remote +build:remote --define=EXECUTOR=remote + + # Setup the remote build execution servers. +build:remote --remote_cache=remotebuildexecution.googleapis.com +build:remote --remote_executor=remotebuildexecution.googleapis.com +build:remote --tls_enabled=true +build:remote --auth_enabled=true +build:remote --remote_timeout=3600 +build:remote --jobs=50 + + # Setup the toolchain and platform for the remote build execution. The platform +# is automatically configured by the "rbe_autoconfig" rule in the project workpsace. +build:remote --host_javabase=@rbe_ubuntu1604_angular//java:jdk +build:remote --javabase=@rbe_ubuntu1604_angular//java:jdk +build:remote --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8 +build:remote --java_toolchain=@bazel_tools//tools/jdk:toolchain_hostjdk8 +build:remote --crosstool_top=@rbe_ubuntu1604_angular//cc:toolchain +build:remote --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 +build:remote --extra_toolchains=@rbe_ubuntu1604_angular//config:cc-toolchain +build:remote --extra_execution_platforms=//tools:rbe_ubuntu1604-angular +build:remote --host_platform=//tools:rbe_ubuntu1604-angular +build:remote --platforms=//tools:rbe_ubuntu1604-angular + + # Setup Build Event Service +build:remote --bes_backend=buildeventservice.googleapis.com +build:remote --bes_timeout=30s +build:remote --bes_results_url="https://source.cloud.google.com/results/invocations/" + + # Set remote caching settings +build:remote --remote_accept_cached=true + +#################################################### +# User bazel configuration +# NOTE: This needs to be the *last* entry in the config. +#################################################### + +# Load any settings which are specific to the current user. Needs to be *last* statement +# in this config, as the user configuration should be able to overwrite flags from this file. +try-import .bazelrc.user diff --git a/.circleci/config.yml b/.circleci/config.yml index c02a15a576f5..203e9386d302 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,115 +7,277 @@ # To validate changes, use an online parser, eg. # http://yaml-online-parser.appspot.com/ +version: 2.1 + # Variables ## IMPORTANT -# If you change the `docker_image` version, also change the `cache_key` suffix and the version of -# `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file. -var_1: &docker_image angular/ngcontainer:0.3.3 -var_2: &cache_key angular_devkit-{{ checksum "yarn.lock" }}-0.3.3-2 - -# Settings common to each job -anchor_1: &defaults - working_directory: ~/ng - docker: - - image: *docker_image - -# After checkout, rebase on top of master. -# Similar to travis behavior, but not quite the same. -# See https://discuss.circleci.com/t/1662 -anchor_2: &post_checkout - post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge" -anchor_3: &root_package_lock_key - key: *cache_key -anchor_4: &attach_options +# If you change the cache key prefix, also sync the restore_cache fallback to match. +# Keep the static part of the cache key as prefix to enable correct fallbacks. +# See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI. +var_1: &cache_key angular_devkit-0.11.0-{{ checksum "yarn.lock" }} +var_2: &default_nodeversion "12.1" +var_3: &attach_options at: . +var_4: &ignore_pull_requests + filters: + branches: + ignore: + - /pull\/.*/ + +# 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: circleci/node:<< parameters.nodeversion >> + working_directory: ~/ng + + test-executor: + parameters: + nodeversion: + type: string + default: *default_nodeversion + docker: + - image: circleci/node:<< parameters.nodeversion >>-browsers + working_directory: ~/ng + environment: + NPM_CONFIG_PREFIX: ~/.npm-global + resource_class: xlarge + + windows-executor: + working_directory: ~/ng + resource_class: windows.medium + shell: powershell.exe -ExecutionPolicy Bypass + machine: + image: windows-server-2019 + +# Command Definitions +# https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-commands +commands: + setup_windows: + steps: + - checkout + - run: + # Need to install node and yarn before, as the base windows image doesn't have anything. + # TODO: remove when CircleCI provides preconfigured node images/VMs. + name: Setup windows node environment + command: ./.circleci/windows-env.ps1 + # TODO: remove commands other than the e2e runner when workspaces on windows are well supported. + - run: + name: Rebase PR on target branch + command: > + if (Test-Path env:CIRCLE_PR_NUMBER) { + git config user.name "angular-ci" + git config user.email "angular-ci" + node tools\rebase-pr.js angular/angular-cli $env:CIRCLE_PR_NUMBER } + - run: node --version + - run: yarn --version + - run: yarn install --frozen-lockfile + + 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_PR_NUMBER}" ]]; 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."; # Job definitions -version: 2 jobs: install: - <<: *defaults + executor: action-executor steps: - - checkout: *post_checkout - - restore_cache: *root_package_lock_key + - 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 + # This fallback should be the cache_key without variables. + - angular_devkit-0.11.0- - run: yarn install --frozen-lockfile - persist_to_workspace: root: . paths: - ./* - save_cache: - <<: *root_package_lock_key + key: *cache_key paths: - ~/.cache/yarn - + lint: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - - run: npm run lint + - run: yarn lint + - run: 'yarn bazel:format -mode=check || + (echo "BUILD files not formatted. Please run ''yarn bazel:format''" ; exit 1)' + # Run the skylark linter to check our Bazel rules + - run: 'yarn bazel:lint || + (echo -e "\n.bzl files have lint errors. Please run ''yarn bazel:lint-fix''"; exit 1)' validate: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - - run: npm run validate -- --ci + - run: yarn validate --ci test: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - - run: npm run test -- --code-coverage --full + - run: yarn test --full test-large: - <<: *defaults + parameters: + ivy: + type: boolean + default: false + glob: + type: string + default: "" + executor: test-executor resource_class: large parallelism: 4 steps: - attach_workspace: *attach_options - - run: npm run webdriver-update-circleci - - run: npm run test-large -- --code-coverage --full --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} + - run: yarn webdriver-update + - run: yarn test-large --full <<# parameters.ivy >>--ivy<> <<# parameters.glob >>--glob="<< parameters.glob >>"<> --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} e2e-cli: - <<: *defaults - environment: - BASH_ENV: ~/.profile - resource_class: xlarge + parameters: + ivy: + type: boolean + default: false + snapshots: + type: boolean + default: false + executor: test-executor parallelism: 4 steps: - attach_workspace: *attach_options - - run: npm install --global npm@6 - - run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} + - run: + name: Initialize Environment + command: ./.circleci/env.sh + - run: + name: Execute CLI E2E Tests + command: PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.ivy >>--ivy<> <<# parameters.snapshots >>--ng-snapshots<> + + e2e-cli-node-10: + executor: + name: test-executor + nodeversion: "10.12" + parallelism: 4 + steps: + - attach_workspace: *attach_options + - run: + name: Initialize Environment + command: | + ./.circleci/env.sh + # Ensure latest npm version to support local package repository + PATH=~/.npm-global/bin:$PATH npm install --global npm + - run: PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} + + test-browsers: + executor: + name: test-executor + environment: + E2E_BROWSERS: true + steps: + - attach_workspace: *attach_options + - 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: PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e ./tests/legacy-cli/e2e/tests/misc/browsers.ts + - run: ./scripts/saucelabs/stop-tunnel.sh build: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - - run: npm run admin -- build + - run: yarn build + + # This is where we put all the misbehaving and flaky tests so we can fine-tune their conditions + # and rerun them faster. + flake-jail: + executor: action-executor + steps: + - attach_workspace: *attach_options + - run: yarn webdriver-update + - run: yarn test-large --full --flakey + - run: yarn test-large --full --flakey --ivy=true build-bazel: - <<: *defaults - resource_class: large + executor: action-executor + resource_class: xlarge steps: - attach_workspace: *attach_options + - setup_bazel_rbe - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - run: bazel test //packages/... + - run: yarn bazel:test snapshot_publish: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - run: name: Decrypt Credentials + # Note: when changing the image, you might have to re-encrypt the credentials with a + # matching version of openssl. + # See https://stackoverflow.com/a/43847627/2116927 for more info. command: | - openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/github_token + openssl aes-256-cbc -d -in .circleci/github_token -k "${KEY}" -out ~/github_token -md md5 - run: name: Deployment to Snapshot command: | - npm run admin -- snapshots --verbose --githubTokenFile=${HOME}/github_token + yarn admin snapshots --verbose --githubTokenFile=${HOME}/github_token publish: - <<: *defaults + executor: action-executor steps: - attach_workspace: *attach_options - run: @@ -125,7 +287,35 @@ jobs: - run: name: Deployment to NPM command: | - npm run admin -- publish --verbose + yarn admin publish --verbose + + # Windows jobs + # CircleCI support for Windows jobs is still in preview. + # Docs: https://github.com/CircleCI-Public/windows-preview-docs + test-win: + executor: windows-executor + # Skipping cache and workspace for now because it takes 10x longer than on linux. + # TODO: when/if CircleCI makes them faster, use cache and workspaces fully. + # Notes: + # - windows needs its own cache key because binaries in node_modules are different. + # - windows might need its own workspace for the same reason. + # - get cache dir on windows via `yarn cache dir` (was `C:\Users\circleci\AppData\Local\Yarn\Cache\v4` last time) + steps: + - setup_windows + # Build and test should be on their own jobs, but restoring workspaces is too slow + # so we do it here. + - run: yarn build + - run: yarn test --full + # Run partial e2e suite on PRs only. Master will run the full e2e suite with sharding. + - run: if (Test-Path env:CIRCLE_PR_NUMBER) { node tests\legacy-cli\run_e2e.js "--glob=tests/{basic,ivy}/**" } + + e2e-cli-win: + executor: windows-executor + parallelism: 4 + steps: + - setup_windows + - run: yarn build + - run: node tests\legacy-cli\run_e2e.js --nb-shards=$env:CIRCLE_NODE_TOTAL --shard=$env:CIRCLE_NODE_INDEX workflows: version: 2 @@ -140,33 +330,79 @@ workflows: - install - build: requires: - - lint - - validate + - install + filters: + branches: + ignore: + - /docs-preview/ - build-bazel: requires: - - lint - - validate + - build - test: requires: - build + - test-win: + requires: + - test + - test-large: + requires: + - build - test-large: + name: test-large-ivy + ivy: true + glob: "packages/angular_devkit/build_angular/test/browser/*_spec_large.ts" requires: - build - e2e-cli: + post-steps: + - store_artifacts: + path: /tmp/dist + destination: cli/new-production + requires: + - build + - e2e-cli: + name: e2e-cli-ivy + ivy: true + requires: + - build + - e2e-cli: + name: e2e-cli-ng-snapshots + snapshots: true + requires: + - e2e-cli + pre-steps: + - run: + name: Don't run expensive e2e snapshots tests for forks other than renovate-bot and angular + command: > + if [[ "$CIRCLE_PR_USERNAME" != "renovate-bot" ]] && + [[ "$CIRCLE_PROJECT_USERNAME" != "angular" || $CIRCLE_BRANCH != "master" ]]; then + circleci step halt + fi + - e2e-cli-node-10: + <<: *ignore_pull_requests + requires: + - e2e-cli + - e2e-cli-win: + <<: *ignore_pull_requests + requires: + - e2e-cli + - test-browsers: + requires: + - build + - flake-jail: requires: - build - snapshot_publish: + <<: *ignore_pull_requests requires: - test - build - e2e-cli - filters: - branches: - only: master - publish: requires: - test - build + - e2e-cli - snapshot_publish filters: tags: diff --git a/.circleci/env-helpers.inc.sh b/.circleci/env-helpers.inc.sh new file mode 100644 index 000000000000..5fa1263e112f --- /dev/null +++ b/.circleci/env-helpers.inc.sh @@ -0,0 +1,73 @@ +#################################################################################################### +# 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 new file mode 100755 index 000000000000..49c0ab18326a --- /dev/null +++ b/.circleci/env.sh @@ -0,0 +1,33 @@ +#!/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"; + +#################################################################################################### +# Define SauceLabs environment variables for CircleCI. +#################################################################################################### +setPublicVar SAUCE_USERNAME "angular-ci"; +setSecretVar SAUCE_ACCESS_KEY "9b988f434ff8-fbca-8aa4-4ae3-35442987"; +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 new file mode 100644 index 000000000000..06773903e8d8 Binary files /dev/null and b/.circleci/gcp_token differ diff --git a/.circleci/windows-env.ps1 b/.circleci/windows-env.ps1 new file mode 100644 index 000000000000..1c4fff24e433 --- /dev/null +++ b/.circleci/windows-env.ps1 @@ -0,0 +1,9 @@ +# Install nodejs and yarn via Chocolatey. +choco install nodejs --version 12.1.0 --no-progress +choco install yarn --version 1.16.0 --no-progress + +# Add PATH modifications to the Powershell profile. This is the win equivalent of .bash_profile. +# https://docs.microsoft.com/en-us/previous-versions//bb613488(v=vs.85) +new-item -path $profile -itemtype file -force +# Paths for nodejs, npm, and yarn. Use single quotes to prevent interpolation. +Add-Content $profile '$Env:path += ";C:\Program Files\nodejs\;C:\Users\circleci\AppData\Roaming\npm\;C:\Program Files (x86)\Yarn\bin\;"' diff --git a/.editorconfig b/.editorconfig index 36e19acf952e..1c658dfbc777 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..de32b85d6693 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# JS and TS files must always use LF for tools to work +*.js eol=lf +*.ts eol=lf +*.json eol=lf +*.css eol=lf +*.scss eol=lf +*.less eol=lf +*.html eol=lf +*.svg eol=lf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 524740f30080..b5135def5125 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,56 +1,10 @@ - -### Bug Report or Feature Request (mark with an `x`) -``` -- [ ] bug report -> please search issues before submitting -- [ ] feature request -``` +🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 -### Command (mark with an `x`) -``` -- [ ] new -- [ ] build -- [ ] serve -- [ ] test -- [ ] e2e -- [ ] generate -- [ ] add -- [ ] update -- [ ] lint -- [ ] xi18n -- [ ] run -- [ ] config -- [ ] help -- [ ] version -- [ ] doc -``` +Please help us process issues more efficiently by filing an +issue using one of the following templates: -### Versions - +https://github.com/angular/angular-cli/issues/new/choose +Thank you! -### Repro steps - - - -### The log given by the failure - - - -### Desired functionality - - - -### Mention any other details that might be useful - +🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md new file mode 100644 index 000000000000..b8249e87c28b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.md @@ -0,0 +1,82 @@ +--- +name: "\U0001F41EBug report" +about: Report a bug in Angular CLI +--- + + + +# 🐞 Bug report + +### Command (mark with an `x`) + + +``` +- [ ] new +- [ ] build +- [ ] serve +- [ ] test +- [ ] e2e +- [ ] generate +- [ ] add +- [ ] update +- [ ] lint +- [ ] xi18n +- [ ] run +- [ ] config +- [ ] help +- [ ] version +- [ ] doc +``` + +### Is this a regression? + + + Yes, the previous version in which this bug was not present was: .... + + +### Description + + A clear and concise description of the problem... + + +## 🔬 Minimal Reproduction + + +## 🔥 Exception or Error +

+
+
+
+
+ + +## 🌍 Your Environment +

+
+
+
+
+ +**Anything else relevant?** + + + diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.md b/.github/ISSUE_TEMPLATE/2-feature-request.md new file mode 100644 index 000000000000..4c8292f45157 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.md @@ -0,0 +1,49 @@ +--- +name: "\U0001F680Feature request" +about: Suggest a feature for Angular CLI + +--- + + + +# 🚀 Feature request + + +### Command (mark with an `x`) + + +``` +- [ ] new +- [ ] build +- [ ] serve +- [ ] test +- [ ] e2e +- [ ] generate +- [ ] add +- [ ] update +- [ ] lint +- [ ] xi18n +- [ ] run +- [ ] config +- [ ] help +- [ ] version +- [ ] doc +``` + +### Description + A clear and concise description of the problem or missing capability... + + +### Describe the solution you'd like + If you have a solution in mind, please describe it. + + +### Describe alternatives you've considered + Have you considered any alternative solutions or workarounds? diff --git a/.github/ISSUE_TEMPLATE/3-docs-bug.md b/.github/ISSUE_TEMPLATE/3-docs-bug.md new file mode 100644 index 000000000000..7cd9ec28753a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-docs-bug.md @@ -0,0 +1,13 @@ +--- +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 new file mode 100644 index 000000000000..b789da9f6da1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-security-issue-disclosure.md @@ -0,0 +1,11 @@ +--- +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 new file mode 100644 index 000000000000..f6e6e66ff893 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-support-request.md @@ -0,0 +1,16 @@ +--- +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 new file mode 100644 index 000000000000..8a689c55de35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/6-angular-framework.md @@ -0,0 +1,13 @@ +--- +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 new file mode 100644 index 000000000000..b023135b0cc5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/7-angular-material.md @@ -0,0 +1,13 @@ +--- +name: "\U0001F48EAngular Material" +about: Issues and feature requests for Angular Material + +--- + +🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 + +Please file any Angular Material issues at: https://github.com/angular/material2/issues/new + +For the time being, we keep Angular Material issues in a separate repository. + +🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml index ca9d26f7dd2d..c00e502cba2b 100644 --- a/.github/angular-robot.yml +++ b/.github/angular-robot.yml @@ -41,13 +41,13 @@ merge: # list of PR statuses that need to be successful requiredStatuses: - - "continuous-integration/appveyor/pr" - "ci/circleci: build" - "ci/circleci: build-bazel" - "ci/circleci: install" - "ci/circleci: lint" - "ci/circleci: validate" - "ci/circleci: test" + - "ci/circleci: test-win" - "ci/circleci: test-large" # the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable @@ -91,3 +91,9 @@ triage: - - "type: docs" - "comp: *" + +# Size checking +size: + circleCiStatusName: "ci/circleci: e2e-cli" + maxSizeIncrease: 10000 + comment: false diff --git a/.gitignore b/.gitignore index e6c63302db9c..88895c7481f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bazel-* test-project-host-* dist/ +dist-schema/ # IDEs .idea/ diff --git a/.mailmap b/.mailmap index bf8640d5dec9..d10bc39a19a7 100644 --- a/.mailmap +++ b/.mailmap @@ -16,6 +16,8 @@ Charles Lyding Charles Lyding <19598772+clydin@users.noreply.github.com> Filipe Silva Mike Brocchi +Alan Agius + Alan Agius ################################################################################ diff --git a/.monorepo.json b/.monorepo.json index 6a3d96820dff..81367b00b771 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -19,7 +19,7 @@ { "label": "License", "image": "https://img.shields.io/npm/l/@angular/cli.svg", - "url": "https://github.com/angular/angular-cli/blob/master/LICENSE" + "url": "/LICENSE" } ], [ @@ -37,17 +37,15 @@ ], "links": { "Gitter": "https://gitter.im/angular/angular-cli", - "Contributing": "https://github.com/angular/angular-cli/blob/master/CONTRIBUTING.md", + "Contributing": "/CONTRIBUTING.md", "Angular CLI": "http://github.com/angular/angular-cli" }, "packages": { "@_/benchmark": { - "version": "0.8.0-rc.0", - "hash": "a9b1f213a4069f789d20021bda616775" + }, + "@_/builders": { }, "devkit": { - "version": "0.8.0-rc.0", - "hash": "5f2b302aa3a1b3f9b6a54a90519f85a0" }, "@angular/cli": { "name": "Angular CLI", @@ -55,18 +53,14 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md" + "url": "/packages/angular/cli/README.md" } ], - "version": "6.2.0-rc.0", - "snapshotRepo": "angular/cli-builds", - "hash": "1418f5d7aa4bde2b7c9339f25087de06" + "snapshotRepo": "angular/cli-builds" }, "@angular/pwa": { "name": "Angular PWA Schematics", "section": "Schematics", - "version": "0.8.0-rc.0", - "hash": "b37d506c657d48379099ccbeeba54da9", "snapshotRepo": "angular/angular-pwa-builds" }, "@angular-devkit/architect": { @@ -74,29 +68,28 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/architect/README.md" + "url": "/packages/angular_devkit/architect/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "64fb30a21ddc98f26b89ffd1dbf828d7", "snapshotRepo": "angular/angular-devkit-architect-builds" }, "@angular-devkit/architect-cli": { "name": "Architect CLI", - "version": "0.8.0-rc.0", - "hash": "9d2161b7ca9044c2286c7d66e10ead12", + "section": "Tooling", "snapshotRepo": "angular/angular-devkit-architect-cli-builds" }, + "@angular-devkit/benchmark": { + "name": "Benchmark", + "section": "Tooling" + }, "@angular-devkit/build-optimizer": { "name": "Build Optimizer", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_optimizer/README.md" + "url": "/packages/angular_devkit/build_optimizer/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "556f1e60c2d5d0ad09ef2e56705e6e4f", "snapshotRepo": "angular/angular-devkit-build-optimizer-builds" }, "@angular-devkit/build-ng-packagr": { @@ -104,11 +97,9 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_ng_packagr/README.md" + "url": "/packages/angular_devkit/build_ng_packagr/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "ac2b1791f5e03267681a444d0a919a26", "snapshotRepo": "angular/angular-devkit-build-ng-packagr-builds" }, "@angular-devkit/build-angular": { @@ -116,11 +107,9 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/README.md" + "url": "/packages/angular_devkit/build_angular/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "4bdacedd086a72f496b737623a47f470", "snapshotRepo": "angular/angular-devkit-build-angular-builds" }, "@angular-devkit/build-webpack": { @@ -128,23 +117,19 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_webpack/README.md" + "url": "/packages/angular_devkit/build_webpack/README.md" } ], - "version": "0.8.0-rc.0", - "snapshotRepo": "angular/angular-devkit-build-webpack-builds", - "hash": "dc4e1b6d135bc9c394169c15cc4902e3" + "snapshotRepo": "angular/angular-devkit-build-webpack-builds" }, "@angular-devkit/core": { "name": "Core", "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/core/README.md" + "url": "/packages/angular_devkit/core/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "0d21af770ad0a24cba535cc296995b7d", "snapshotRepo": "angular/angular-devkit-core-builds" }, "@angular-devkit/schematics": { @@ -152,46 +137,34 @@ "links": [ { "label": "README", - "url": "https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md" + "url": "/packages/angular_devkit/schematics/README.md" } ], - "version": "0.8.0-rc.0", - "hash": "a10bf52f075cc93c8d28d187dcce788d", "snapshotRepo": "angular/angular-devkit-schematics-builds" }, "@angular-devkit/schematics-cli": { "name": "Schematics CLI", "section": "Tooling", - "version": "0.8.0-rc.0", - "hash": "f1ee4ce02cf395e6ef6888fa93ef9d50", "snapshotRepo": "angular/angular-devkit-schematics-cli-builds" }, "@ngtools/webpack": { "name": "Webpack Angular Plugin", - "version": "6.2.0-rc.0", "section": "Misc", - "hash": "9ac09fecd47a8602820ae5abb94ddac9", "snapshotRepo": "angular/ngtools-webpack-builds" }, "@schematics/angular": { "name": "Angular Schematics", "section": "Schematics", - "version": "0.8.0-rc.0", - "hash": "acf5e21cf8dc3d69ab9333480d2007f3", "snapshotRepo": "angular/schematics-angular-builds" }, "@schematics/schematics": { "name": "Schematics Schematics", - "version": "0.8.0-rc.0", "section": "Schematics", - "hash": "45b6ce378bceb13a53b67e2cfbfcb5ab", "snapshotRepo": "angular/schematics-schematics-builds" }, "@schematics/update": { "name": "Package Update Schematics", - "version": "0.8.0-rc.0", "section": "Schematics", - "hash": "e29b2069a90870776b29050bedb9475a", "snapshotRepo": "angular/schematics-update-builds" } } diff --git a/.nvmrc b/.nvmrc index 45a4fb75db86..f599e28b8ab0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8 +10 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..fbcd212375a5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +/etc/api diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000000..5e2863a11f68 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/BUILD b/BUILD index 5b9205d3eef1..a1c5b6b5ac0e 100644 --- a/BUILD +++ b/BUILD @@ -9,30 +9,6 @@ licenses(["notice"]) # MIT License exports_files([ "LICENSE", "tsconfig.json", # @external + "tsconfig-test.json", # @external + "tslint.base.json", # @external ]) - -# NOTE: this will move to node_modules/BUILD in a later release -# @external_begin -filegroup( - name = "node_modules", - srcs = glob( - # Only include files we might use, to reduce the file count and surface size of - # filename problems. - [ - "node_modules/**/*.js", - "node_modules/**/*.json", - "node_modules/**/*.d.ts", - ], - exclude = [ - # e.g. node_modules/adm-zip/test/assets/attributes_test/New folder/hidden.txt - "node_modules/**/test/**", - # e.g. node_modules/xpath/docs/function resolvers.md - "node_modules/**/docs/**", - # e.g. node_modules/puppeteer/.local-chromium/mac-536395/chrome-mac/Chromium.app/Contents/Versions/66.0.3347.0/Chromium Framework.framework/Chromium Framework - "node_modules/**/.*/**", - # Ignore paths with spaces. - "node_modules/**/* *", - ], - ) + glob(["node_modules/.bin/*"]), -) -# @external_end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e93eef54157f..6072228db9a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,13 +12,17 @@ to follow: - [Coding Rules](#rules) - [Commit Message Guidelines](#commit) - [Signing the CLA](#cla) + - [Updating the Public API](#public-api) ## Code of Conduct Help us keep Angular open and inclusive. Please read and follow our [Code of Conduct][coc]. ## Got a Question or Problem? -Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](https://stackoverflow.com/questions/tagged/angular-devkit) where the questions should be tagged with tag `angular-devkit`. +Please, do not open issues for the general support questions as we want to keep GitHub issues for +bug reports and feature requests. You've got much better chances of getting your question answered +on [StackOverflow](https://stackoverflow.com/questions/tagged/angular-devkit) where the questions +should be tagged with tag `angular-cli` or `angular-devkit`. StackOverflow is a much better place to ask questions since: @@ -26,7 +30,8 @@ StackOverflow is a much better place to ask questions since: - questions and answers stay available for public viewing so your question / answer might help someone else - StackOverflow's voting system assures that the best answers are prominently visible. -To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow. +To save your and our time we will be systematically closing all the issues that are requests for +general support and redirecting people to StackOverflow. If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter]. @@ -66,7 +71,7 @@ We will be insisting on a minimal reproduce scenario in order to save maintainer Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. -You can file new issues by filling out our [new issue form](https://github.com/angular/angular-cli/issues/new). +You can file new issues by selecting from our [new issue templates](https://github.com/angular/angular-cli/issues/new/choose) and filling out the issue template. ### Submitting a Pull Request (PR) @@ -180,41 +185,65 @@ If the commit reverts a previous commit, it should begin with `revert: `, follow ### Type Must be one of the following: -* **build**: Changes that affect the build system or external dependencies -* **ci**: Changes to our CI configuration files and scripts -* **docs**: Documentation only changes -* **feat**: A new feature -* **fix**: A bug fix -* **perf**: A code change that improves performance -* **refactor**: A code change that neither fixes a bug nor adds a feature -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **test**: Adding missing tests or correcting existing tests +* **build**: Changes that affect the build system or external dependencies. [2] +* **ci**: Changes to our CI configuration files and scripts. [2] +* **docs**: Documentation only changes. +* **feat**: A new feature. [1] +* **fix**: A bug fix. [1] +* **refactor**: A code change that neither fixes a bug nor adds a feature +* **release**: A release commit. Must only include version changes. [2] +* **revert**: A git commit revert. The description must include the original commit message. [2] +* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). +* **test**: Adding missing tests or correcting existing tests. + + +[1] This type MUST have a scope. See the next section for more information.
+[2] This type MUST NOT have a scope. It only applies to general scripts and tooling. ### Scope The scope should be the name of the npm package affected as perceived by the person reading changelog generated from the commit messages. The following is the list of supported scopes: -* **@angular-devkit/core** +* **@angular/cli** +* **@angular/pwa** +* **@angular-devkit/architect** +* **@angular-devkit/architect-cli** +* **@angular-devkit/build-angular** +* **@angular-devkit/build-ng-packagr** * **@angular-devkit/build-optimizer** +* **@angular-devkit/build-webpack** +* **@angular-devkit/core** * **@angular-devkit/schematics** * **@angular-devkit/schematics-cli** +* **@ngtools/webpack** * **@schematics/angular** * **@schematics/schematics** +* **@schematics/update** -There are currently a few exceptions to the "use package name" rule: - -* **packaging**: used for changes that change the npm package layout in all of our packages, e.g. public path changes, package.json changes done to all packages, d.ts file/format changes, changes to bundles, etc. -* **changelog**: used for updating the release notes in CHANGELOG.md -* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`) ### Subject The subject contains succinct description of the change: * use the imperative, present tense: "change" not "changed" nor "changes" * don't capitalize first letter +* be concise and direct * no dot (.) at the end +### Examples +Examples of valid commit messages: + +* `fix(@angular/cli): prevent the flubber from grassing` +* `refactor(@schematics/angular): move all JSON classes together` + +Examples of invalid commit messages: +* `fix(@angular/cli): add a new XYZ command` + + This is a feature, not a fix. +* `ci(@angular/cli): fix publishing workflow` + + The `ci` type cannot have a scope. + ### Body Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. @@ -240,9 +269,44 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise [coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# [corporate-cla]: http://code.google.com/legal/corporate-cla-v1.0.html -[dev-doc]: ttps://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md#development-hints-for-working-on-angular-cli +[dev-doc]: https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md#development-hints-for-working-on-angular-cli [GitHub]: https://github.com/angular/angular-cli [gitter]: https://gitter.im/angular/angular-cli [individual-cla]: http://code.google.com/legal/individual-cla-v1.0.html [js-style-guide]: https://google.github.io/styleguide/jsguide.html [stackoverflow]: http://stackoverflow.com/questions/tagged/angular-devkit + +## Updating the Public API +Our Public API is protected with TS API Guardian. This is a tool that keeps track of public API surface of our packages. + +To test if your change effect the public API you need to run the API guardian on that particular package. + +For example in case `@angular-devkit/core` package was modified you need to run: + +```bash +yarn bazel test //etc/api:angular_devkit_core_api +``` + +You can also test all packages by running: +```bash +yarn bazel test //etc/api ... +``` + +If you modified the public API, the test will fail. To update the golden files you need to run: + +```bash +yarn bazel run //etc/api:angular_devkit_core_api.accept +``` + +**Note**: In some cases we use aliased symbols to create namespaces. + +Example: +```javascript +import * as foo from './foo'; + +export { foo }; +``` +There are currently not supported by the API guardian. +To overcome this limitation we created `_golden-api.ts` in certain packages. + +When adding a new API, it might be the case that you need to add it to `_golden-api.ts`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..8fef94100787 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,2 @@ +FROM node:10.12 +ENTRYPOINT [ "sh" ] diff --git a/README.md b/README.md index 762e1ad325e8..2b50b1394f92 100644 --- a/README.md +++ b/README.md @@ -10,39 +10,63 @@ Any changes to README.md directly will result in a failure on CI. --> -# Angular DevKit +# Angular CLI ### Development tools and libraries specialized for Angular This is the home of the DevKit and the Angular CLI code. You can find the Angular CLI specific README -[here](https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md). +[here](/packages/angular/cli/README.md). [![CircleCI branch](https://img.shields.io/circleci/project/github/angular/angular-cli/master.svg?label=circleci)](https://circleci.com/gh/angular/angular-cli) [![Dependency Status](https://david-dm.org/angular/angular-cli.svg)](https://david-dm.org/angular/angular-cli) [![devDependency Status](https://david-dm.org/angular/angular-cli/dev-status.svg)](https://david-dm.org/angular/angular-cli?type=dev) -[![License](https://img.shields.io/npm/l/@angular/cli.svg)](https://github.com/angular/angular-cli/blob/master/LICENSE) +[![License](https://img.shields.io/npm/l/@angular/cli.svg)](/LICENSE) [![GitHub forks](https://img.shields.io/github/forks/angular/angular-cli.svg?style=social&label=Fork)](https://github.com/angular/angular-cli/fork) [![GitHub stars](https://img.shields.io/github/stars/angular/angular-cli.svg?style=social&label=Star)](https://github.com/angular/angular-cli) ----- - -This is the home for all the tools and libraries built to assist developers with their Angular applications. - ### Quick Links -[Gitter](https://gitter.im/angular/angular-cli) | [Contributing](https://github.com/angular/angular-cli/blob/master/CONTRIBUTING.md) | [Angular CLI](http://github.com/angular/angular-cli) | +[Gitter](https://gitter.im/angular/angular-cli) | [Contributing](/CONTRIBUTING.md) | [Angular CLI](http://github.com/angular/angular-cli) | |---|---|---| +---- + +## The Goal of Angular CLI + +The Angular CLI creates, manages, builds and test your Angular projects. It's built on top of the +Angular DevKit. ## The Goal of DevKit -Our goal is to provide a large set of libraries that can be used to manage, develop, deploy and +DevKit's goal is to provide a large set of libraries that can be used to manage, develop, deploy and analyze your code. -This is the extension of the Angular CLI Project. Once this set of tools is done, the Angular CLI -as it is today will become one of many interfaces available to perform those tasks. Everything -will also be available to third party tools and IDEs. +# Getting Started - Local Development + +## Installation +To get started locally, follow these instructions: + +1. If you haven't done it already, [make a fork of this repo](https://github.com/angular/angular-cli/fork). +1. Clone to your local computer using `git`. +1. Make sure that you have Node 10.9 or later installed. See instructions [here](https://nodejs.org/en/download/). +1. Make sure that you have `yarn` installed; see instructions [here](https://yarnpkg.com/lang/en/docs/install/). +1. Run `yarn` (no arguments) from the root of your clone of this project. +1. Run `yarn link` to add all custom scripts we use to your global install. +## Creating New Packages +Adding a package to this repository means running two separate commands: + +1. `schematics devkit:package PACKAGE_NAME`. This will update the `.monorepo` file, and create the + base files for the new package (package.json, src/index, etc). +1. `devkit-admin templates`. This will update the README and all other template files that might + have changed when adding a new package. + +For private packages, you will need to add a `"private": true` key to your package.json manually. +This will require re-running the template admin script. + +# Packages + +This is a monorepo which contains many tools and packages: @@ -50,26 +74,23 @@ will also be available to third party tools and IDEs. | Project | Package | Version | Links | |---|---|---|---| -**Angular CLI** | [`@angular/cli`](https://npmjs.com/package/@angular/cli) | [![latest](https://img.shields.io/npm/v/%40angular%2Fcli/latest.svg)](https://npmjs.com/package/@angular/cli) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/cli-builds) +**Angular CLI** | [`@angular/cli`](https://npmjs.com/package/@angular/cli) | [![latest](https://img.shields.io/npm/v/%40angular%2Fcli/latest.svg)](https://npmjs.com/package/@angular/cli) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular/cli/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/cli-builds) +**Architect CLI** | [`@angular-devkit/architect-cli`](https://npmjs.com/package/@angular-devkit/architect-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/architect-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-cli-builds) **Schematics CLI** | [`@angular-devkit/schematics-cli`](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-cli-builds) - ## Packages -This is a monorepo which contains many packages: - | Project | Package | Version | Links | |---|---|---|---| -**Architect** | [`@angular-devkit/architect`](https://npmjs.com/package/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](https://npmjs.com/package/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/architect/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-builds) -**Architect CLI** | [`@angular-devkit/architect-cli`](https://npmjs.com/package/@angular-devkit/architect-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/architect-cli) | [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-cli-builds) -**Build Angular** | [`@angular-devkit/build-angular`](https://npmjs.com/package/@angular-devkit/build-angular) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-angular/latest.svg)](https://npmjs.com/package/@angular-devkit/build-angular) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-angular-builds) -**Build NgPackagr** | [`@angular-devkit/build-ng-packagr`](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-ng-packagr/latest.svg)](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_ng_packagr/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-ng-packagr-builds) -**Build Optimizer** | [`@angular-devkit/build-optimizer`](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_optimizer/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-optimizer-builds) -**Build Webpack** | [`@angular-devkit/build-webpack`](https://npmjs.com/package/@angular-devkit/build-webpack) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-webpack/latest.svg)](https://npmjs.com/package/@angular-devkit/build-webpack) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_webpack/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-webpack-builds) -**Core** | [`@angular-devkit/core`](https://npmjs.com/package/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](https://npmjs.com/package/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/core/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-core-builds) -**Schematics** | [`@angular-devkit/schematics`](https://npmjs.com/package/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-builds) +**Architect** | [`@angular-devkit/architect`](https://npmjs.com/package/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](https://npmjs.com/package/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/architect/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-architect-builds) +**Build Angular** | [`@angular-devkit/build-angular`](https://npmjs.com/package/@angular-devkit/build-angular) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-angular/latest.svg)](https://npmjs.com/package/@angular-devkit/build-angular) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_angular/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-angular-builds) +**Build NgPackagr** | [`@angular-devkit/build-ng-packagr`](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-ng-packagr/latest.svg)](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_ng_packagr/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-ng-packagr-builds) +**Build Optimizer** | [`@angular-devkit/build-optimizer`](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_optimizer/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-optimizer-builds) +**Build Webpack** | [`@angular-devkit/build-webpack`](https://npmjs.com/package/@angular-devkit/build-webpack) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-webpack/latest.svg)](https://npmjs.com/package/@angular-devkit/build-webpack) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/build_webpack/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-build-webpack-builds) +**Core** | [`@angular-devkit/core`](https://npmjs.com/package/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](https://npmjs.com/package/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/core/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-core-builds) +**Schematics** | [`@angular-devkit/schematics`](https://npmjs.com/package/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/angular_devkit/schematics/README.md) [![snapshot](https://img.shields.io/badge/snapshot--blue.svg)](https://github.com/angular/angular-devkit-schematics-builds) #### Schematics diff --git a/WORKSPACE b/WORKSPACE index 3647c4e935d3..e3a274dae0be 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,66 +1,113 @@ -workspace(name = "angular_devkit") +workspace(name = "angular_cli") + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -# We get Buildifier from here. http_archive( - name = "com_github_bazelbuild_buildtools", - url = "https://github.com/bazelbuild/buildtools/archive/0.15.0.zip", - strip_prefix = "buildtools-0.15.0", - sha256 = "76d1837a86fa6ef5b4a07438f8489f00bfa1b841e5643b618e01232ba884b1fe", + name = "build_bazel_rules_nodejs", + sha256 = "fb87ed5965cef93188af9a7287511639403f4b0da418961ce6defb9dcf658f51", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.7/rules_nodejs-0.27.7.tar.gz"], ) -load("@com_github_bazelbuild_buildtools//buildifier:deps.bzl", "buildifier_dependencies") -buildifier_dependencies() - -# The Go toolchain is used for Buildifier. -# rules_typescript_dependencies() also tries to load it but we add it explicitely so we -# don't have hidden dependencies. -# This also means we need to load it before rules_typescript_dependencies(). -http_archive( - name = "io_bazel_rules_go", - url = "https://github.com/bazelbuild/rules_go/archive/0.14.0.zip", - strip_prefix = "rules_go-0.14.0", - sha256 = "9bd7c2743f014e4e112b671098ba1da6aec036fe07093b10ca39a9f81ec5cc33", +# We use protocol buffers for the Build Event Protocol +git_repository( + name = "com_google_protobuf", + commit = "beaeaeda34e97a6ff9735b33a66e011102ab506b", + remote = "https://github.com/protocolbuffers/protobuf", ) -load("@io_bazel_rules_go//go:def.bzl", "go_register_toolchains", "go_rules_dependencies") -go_rules_dependencies() -go_register_toolchains() +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") -# We need a minimum of this version to include https://github.com/bazelbuild/rules_nodejs/pull/281. -http_archive( - name = "build_bazel_rules_nodejs", - url = "https://github.com/bazelbuild/rules_nodejs/archive/c75e3dd0571b0937e3ce0c4f0e6b6b50d90468f0.zip", - strip_prefix = "rules_nodejs-c75e3dd0571b0937e3ce0c4f0e6b6b50d90468f0", - sha256 = "b78506ddaed7c682027f873d2bd50086a28570b3187da9fa16fe1672eed3015e", +protobuf_deps() + +load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install") + +# 0.18.0 is needed for .bazelignore +check_bazel_version(minimum_bazel_version = "0.18.0") + +node_repositories( + node_repositories = { + "10.9.0-darwin_amd64": ( + "node-v10.9.0-darwin-x64.tar.gz", + "node-v10.9.0-darwin-x64", + "3c4fe75dacfcc495a432a7ba2dec9045cff359af2a5d7d0429c84a424ef686fc", + ), + "10.9.0-linux_amd64": ( + "node-v10.9.0-linux-x64.tar.xz", + "node-v10.9.0-linux-x64", + "c5acb8b7055ee0b6ac653dc4e458c5db45348cecc564b388f4ed1def84a329ff", + ), + "10.9.0-windows_amd64": ( + "node-v10.9.0-win-x64.zip", + "node-v10.9.0-win-x64", + "6a75cdbb69d62ed242d6cbf0238a470bcbf628567ee339d4d098a5efcda2401e", + ), + }, + node_version = "10.9.0", + yarn_repositories = { + "1.9.2": ( + "yarn-v1.9.2.tar.gz", + "yarn-v1.9.2", + "3ad69cc7f68159a562c676e21998eb21b44138cae7e8fe0749a7d620cf940204", + ), + }, + yarn_version = "1.9.2", ) -# Load the TypeScript rules, its dependencies, and setup the workspace. -http_archive( - name = "build_bazel_rules_typescript", - url = "https://github.com/bazelbuild/rules_typescript/archive/0.16.1.zip", - strip_prefix = "rules_typescript-0.16.1", - sha256 = "5b2b0bc63cfcffe7bf97cad2dad3b26a73362f806de66207051f66c87956a995", +yarn_install( + name = "npm", + data = [ + "//:tools/yarn/check-yarn.js", + ], + package_json = "//:package.json", + yarn_lock = "//:yarn.lock", ) -load("@build_bazel_rules_typescript//:package.bzl", "rules_typescript_dependencies") -# build_bazel_rules_nodejs is loaded transitively through rules_typescript_dependencies. -rules_typescript_dependencies() +load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies") + +install_bazel_dependencies() + +load("@npm_bazel_typescript//:defs.bzl", "ts_setup_workspace") -load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace") ts_setup_workspace() -# Load the nodejs dependencies, check minimum Bazel version, and define the local node_modules. -load("@build_bazel_rules_nodejs//:package.bzl", "rules_nodejs_dependencies") -rules_nodejs_dependencies() +# Load karma dependencies +load("@npm_bazel_karma//:package.bzl", "rules_karma_dependencies") -load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") -check_bazel_version("0.15.0") -node_repositories( - package_json = ["//:package.json"], - preserve_symlinks = True, +rules_karma_dependencies() + +# Setup the rules_webtesting toolchain +load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories") + +web_test_repositories() + +########################## +# Remote Execution Setup # +########################## +# Bring in bazel_toolchains for RBE setup configuration. +http_archive( + name = "bazel_toolchains", + sha256 = "54764b510cf45754c01ac65c9ba83e5f8fc8a033b8296ef74c4e4d6d1dbfaf21", + strip_prefix = "bazel-toolchains-d67435097bd65153a592ecdcc83094474914c205", + urls = ["https://github.com/xingao267/bazel-toolchains/archive/d67435097bd65153a592ecdcc83094474914c205.tar.gz"], ) -local_repository( - name = "rxjs", - path = "node_modules/rxjs/src", +load("@bazel_toolchains//rules:environments.bzl", "clang_env") +load("@bazel_toolchains//rules:rbe_repo.bzl", "rbe_autoconfig") + +rbe_autoconfig( + name = "rbe_ubuntu1604_angular", + # The sha256 of marketplace.gcr.io/google/rbe-ubuntu16-04 container that is + # used by rbe_autoconfig() to pair toolchain configs in the @bazel_toolchains repo. + base_container_digest = "sha256:677c1317f14c6fd5eba2fd8ec645bfdc5119f64b3e5e944e13c89e0525cc8ad1", + # Note that if you change the `digest`, you might also need to update the + # `base_container_digest` to make sure marketplace.gcr.io/google/rbe-ubuntu16-04-webtest: + # and marketplace.gcr.io/google/rbe-ubuntu16-04: have + # the same Clang and JDK installed. + # Clang is needed because of the dependency on @com_google_protobuf. + # Java is needed for the Bazel's test executor Java tool. + digest = "sha256:74a8e9dca4781d5f277a7bd8e7ea7ed0f5906c79c9cd996205b6d32f090c62f3", + env = clang_env(), + registry = "marketplace.gcr.io", + repository = "google/rbe-ubuntu16-04-webtest", ) diff --git a/benchmark/aio/.gitignore b/benchmark/aio/.gitignore new file mode 100644 index 000000000000..3c85010e11ea --- /dev/null +++ b/benchmark/aio/.gitignore @@ -0,0 +1 @@ +angular/ \ No newline at end of file diff --git a/benchmark/aio/package.json b/benchmark/aio/package.json new file mode 100644 index 000000000000..fce9b14e36f7 --- /dev/null +++ b/benchmark/aio/package.json @@ -0,0 +1,18 @@ +{ + "name": "aio-benchmark", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "initialize": "yarn clone && yarn setup && yarn update", + "clone": "(git clone https://github.com/angular/angular --depth 1 || true) && cd angular && git fetch origin dd2a650c3455f3bc0a88f8181758a84aacb25fea && git checkout -f FETCH_HEAD", + "setup": "cd angular && yarn && cd aio && yarn && yarn setup", + "update": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", + "//": "Shouldn't need to install the package twice, but the first install seems to leave two @ngtools/webpack installs around.", + "postupdate": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", + "benchmark": "cd angular/aio && benchmark --verbose -- yarn ~~build --configuration=stable" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 000000000000..6f3d6c17fbd2 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,17 @@ +# `/bin` Folder + +This folder includes binaries that are linked when globally linking this repository. + +Each file in this directory follows this pattern: + +1. JavaScript only. +1. Requires `../lib/bootstrap-local.js` to bootstrap TypeScript and Node integration. +1. Requires `../lib/packages` and use the package metadata to find the binary script for the +package the script is bootstrapping. +1. Call out main, or simply require the file if it has no export. + +`devkit-admin` does not follow this pattern as it needs to setup logging and run some localized +logic. + +In order to add a new script, you should make sure it's in the root `package.json`, so people +linking this repo get a reference to the script. diff --git a/bin/purify b/bin/benchmark similarity index 60% rename from bin/purify rename to bin/benchmark index 810b4d6820b9..4ada663d8bfc 100755 --- a/bin/purify +++ b/bin/benchmark @@ -11,4 +11,9 @@ require('../lib/bootstrap-local'); const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/build-optimizer'].bin['purify']); +const main = require(packages['@angular-devkit/benchmark'].bin['benchmark']).main; + +const args = process.argv.slice(2); +main({ args }) + .then(exitCode => process.exitCode = exitCode) + .catch(e => { throw (e); }); diff --git a/bin/devkit-admin b/bin/devkit-admin index be8719fd60ca..f356b0d42e96 100755 --- a/bin/devkit-admin +++ b/bin/devkit-admin @@ -25,6 +25,7 @@ const args = minimist(process.argv.slice(2), { const scriptName = args._.shift(); const scriptPath = path.join('../scripts', scriptName); +const cwd = process.cwd(); process.chdir(path.join(__dirname, '..')); @@ -64,14 +65,13 @@ try { try { Promise.resolve() - .then(() => require(scriptPath).default(args, logger)) + .then(() => require(scriptPath).default(args, logger, cwd)) .then(exitCode => process.exit(exitCode || 0)) .catch(err => { - logger.fatal(err.stack); + logger.fatal(err && err.stack); process.exit(99); }); } catch (err) { logger.fatal(err.stack); process.exit(99); } - diff --git a/bin/schematics b/bin/schematics index 748599df46c9..ea3f5176befa 100755 --- a/bin/schematics +++ b/bin/schematics @@ -11,4 +11,4 @@ require('../lib/bootstrap-local'); const packages = require('../lib/packages').packages; -require(packages['@angular-devkit/schematics-cli'].bin['schematics']); +require(packages['@angular-devkit/schematics-cli'].bin['schematics']).main({ args: process.argv.slice(2) }); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000000..6fdee803e80c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +# `/docs` Folder + +This folder is used for all documentation. It contains a number of subfolders with short +descriptions here. + +## `/docs/design` + +Design documents on GitHub, in Markdown format. The number of design documents available is limited. + +## `/docs/documentation` + +THIS FOLDER IS DEPRECATED AND SHOULD NOT BE UPDATED ANYMORE. Documentation is now available on +angular.io and can be updated on the main [angular/angular](https://github.com/angular/angular) +repo. + +Markdown files used to publish to the [GitHub Wiki](https://github.com/angular/angular-cli/wiki). + +## `/docs/process` + +Description of various caretaker and workflow processes. + +## `/docs/specifications` + +Specifications for support of Angular CLI features. These are meant to be read directly on GitHub +by developers of libraries who want to integrate with the Angular CLI. diff --git a/docs/design/analytics.md b/docs/design/analytics.md new file mode 100644 index 000000000000..5ec9c56adf29 --- /dev/null +++ b/docs/design/analytics.md @@ -0,0 +1,124 @@ +# Usage Metrics Gathering +This document list exactly what is gathered and how. + +Any change to analytics should most probably include a change to this document. + +# Pageview +Each command creates a pageview with the path `/command/${commandName}/${subcommandName}`. IE. +`ng generate component my-component --dryRun` would create a page view with the path +`/command/generate/@schematics_angular/component`. + +We use page views to keep track of sessions more effectively, and to tag events to a page. + +Project names and target names will be removed. +The command `ng run some-project:lint:some-configuration` will create a page view with the path +`/command/run`. + +# Dimensions +Google Analytics Custom Dimensions are used to track system values and flag values. These +dimensions are aggregated automatically on the backend. + +One dimension per flag, and although technically there can be an overlap between 2 commands, for +simplicity it should remain unique across all CLI commands. The dimension is the value of the +`x-user-analytics` field in the `schema.json` files. + +To create a new dimension (tracking a new flag): + +1. Create the dimension on analytics.google.com first. Dimensions are not tracked if they aren't + defined on GA. +1. Use the ID of the dimension as the `x-user-analytics` value in the `schema.json` file. +1. Add a new row to the table below in the same PR as the one adding the dimension to the code. +1. New dimensions PRs need to be approved by [bradgreen@google.com](mailto:bradgreen@google.com), + [stephenfluin@google.com](mailto:stephenfluin@google.com) and + [iminar@google.com](mailto:iminar@google.com). **This is not negotiable.** + +**DO NOT ADD `x-user-analytics` FOR VALUES THAT ARE USER IDENTIFIABLE (PII), FOR EXAMPLE A +PROJECT NAME TO BUILD OR A MODULE NAME.** + +Note: There's a limit of 20 custom dimensions. + +### List Of All Dimensions + +| Id | Flag | Type | +|:---:|:---|:---| +| 1 | `CPU Count` | `number` | +| 2 | `CPU Speed` | `number` | +| 3 | `RAM (In GB)` | `number` | +| 4 | `Node Version` | `number` | +| 5 | `Flag: --style` | `string` | +| 6 | `--collection` | `string` | +| 7 | `--buildEventLog` | `boolean` | +| 8 | `Flag: --enableIvy` | `boolean` | +| 9 | `Flag: --inlineStyle` | `boolean` | +| 10 | `Flag: --inlineTemplate` | `boolean` | +| 11 | `Flag: --viewEncapsulation` | `string` | +| 12 | `Flag: --skipTests` | `boolean` | +| 13 | `Flag: --aot` | `boolean` | +| 14 | `Flag: --minimal` | `boolean` | +| 15 | `Flag: --lintFix` | `boolean` | +| 16 | `Flag: --optimization` | `boolean` | +| 17 | `Flag: --routing` | `boolean` | +| 18 | `Flag: --skipImport` | `boolean` | +| 19 | `Flag: --export` | `boolean` | +| 20 | `Build Errors (comma separated)` | `string` | + + +# Metrics + +### List of All Metrics + +| Id | Flag | Type | +|:---:|:---|:---| +| 1 | `NgComponentCount` | `number` | +| 2 | `UNUSED_2` | `none` | +| 3 | `UNUSED_3` | `none` | +| 4 | `UNUSED_4` | `none` | +| 5 | `Build Time` | `number` | +| 6 | `NgOnInit Count` | `number` | +| 7 | `Initial Chunk Size` | `number` | +| 8 | `Total Chunk Count` | `number` | +| 9 | `Total Chunk Size` | `number` | +| 10 | `Lazy Chunk Count` | `number` | +| 11 | `Lazy Chunk Size` | `number` | +| 12 | `Asset Count` | `number` | +| 13 | `Asset Size` | `number` | +| 14 | ` Polyfill Size` | `number` | +| 15 | ` Css Size` | `number` | + + +# Operating System and Node Version +A User Agent string is built to "fool" Google Analytics into reading the Operating System and +version fields from it. The base dimensions are used for those. + +Node version is our App ID, but a dimension is also used to get the numeric MAJOR.MINOR of node. + +# Debugging +Using `DEBUG=universal-analytics` will report all calls to the universal-analytics library, +including queuing events and sending them to the server. + +Using `DEBUG=ng:analytics` will report additional information regarding initialization and +decisions made during the usage analytics process, e.g. if the user has analytics disabled. + +Using `DEBUG=ng:analytics:command` will show the decisions made by the command runner. + +Using `DEBUG=ng:analytics:log` will show what we actually send to GA. + +See [the `debug` NPM library](https://www.npmjs.com/package/debug) for more information. + +# Disabling Usage Analytics +There are 2 ways of disabling usage analytics: + +1. using `ng analytics off` (or changing the global configuration file yourself). This is the same + as answering "No" to the prompt. +1. There is an `NG_CLI_ANALYTICS` environment variable that overrides the global configuration. + That flag is a string that represents the User ID. If the string `"false"` is used it will + disable analytics for this run. If the string `"ci"` is used it will show up as a CI run (see + below). + +# CI +A special user named `ci` is used for analytics for tracking CI information. This is a convention +and is in no way enforced. + +Running on CI by default will disable analytics (because of a lack of TTY on STDIN/OUT). It can be +manually enabled using either a global configuration with a value of `ci`, or using the +`NG_CLI_ANALYTICS=ci` environment variable. diff --git a/docs/documentation/add.md b/docs/documentation/add.md index 9406f31a2c2a..1b208274a1e5 100644 --- a/docs/documentation/add.md +++ b/docs/documentation/add.md @@ -1,5 +1,7 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/add).** + # ng add ## Overview diff --git a/docs/documentation/angular-workspace.md b/docs/documentation/angular-workspace.md index 48be62aa0e8f..fbe3761870dc 100644 --- a/docs/documentation/angular-workspace.md +++ b/docs/documentation/angular-workspace.md @@ -1,4 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available in [angular.io](https://angular.io)**. # Angular CLI workspace file (angular.json) schema diff --git a/docs/documentation/build.md b/docs/documentation/build.md index ba25703f56b1..20b765306349 100644 --- a/docs/documentation/build.md +++ b/docs/documentation/build.md @@ -1,4 +1,6 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/build)**. + # ng build @@ -392,7 +394,7 @@ See https://github.com/angular/angular-cli/issues/7797 for details. --stats-json

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

diff --git a/docs/documentation/config.md b/docs/documentation/config.md index bc4d49f32612..6fc6bcc14172 100644 --- a/docs/documentation/config.md +++ b/docs/documentation/config.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/config)**. # ng config diff --git a/docs/documentation/doc.md b/docs/documentation/doc.md index 33fcd402f69c..3defe5f6e8aa 100644 --- a/docs/documentation/doc.md +++ b/docs/documentation/doc.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/doc)**. # ng doc diff --git a/docs/documentation/e2e.md b/docs/documentation/e2e.md index 2f9c0d40a695..85c1bd4c401b 100644 --- a/docs/documentation/e2e.md +++ b/docs/documentation/e2e.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/e2e)**. # ng e2e diff --git a/docs/documentation/eject.md b/docs/documentation/eject.md index 3e2ff0055fe4..9d0ca516438d 100644 --- a/docs/documentation/eject.md +++ b/docs/documentation/eject.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this.**. # ng eject diff --git a/docs/documentation/generate.md b/docs/documentation/generate.md index ad35dc73742c..11ecd055b66a 100644 --- a/docs/documentation/generate.md +++ b/docs/documentation/generate.md @@ -1,10 +1,15 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate ## Overview `ng generate [name]` generates the specified schematic +## Alias +g - `ng g [name]` + ## Available Schematics: - [class](generate/class) - [component](generate/component) @@ -15,7 +20,7 @@ - [module](generate/module) - [pipe](generate/pipe) - [service](generate/service) - + - [appShell](generate/app-shell) - [application](generate/application) - [library](generate/library) - [universal](generate/universal) diff --git a/docs/documentation/generate/app-shell.md b/docs/documentation/generate/app-shell.md index 42308267e804..384b87e58ae5 100644 --- a/docs/documentation/generate/app-shell.md +++ b/docs/documentation/generate/app-shell.md @@ -1,9 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate app-shell ## Overview -Create an app shell. +`ng generate appShell [name]` creates an app shell + +## Alias +app-shell - `ng generate app-shell [name]` ## Options
diff --git a/docs/documentation/generate/application.md b/docs/documentation/generate/application.md index 439d24693cf2..c6e8bfc5e3fd 100644 --- a/docs/documentation/generate/application.md +++ b/docs/documentation/generate/application.md @@ -1,9 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate application ## Overview -Create an Angular application. +`ng generate application [name]` creates an Angular application. + +## Alias +app - `ng generate app [name]` ## Options
diff --git a/docs/documentation/generate/class.md b/docs/documentation/generate/class.md index 4804318f7eff..911a9f3c14f2 100644 --- a/docs/documentation/generate/class.md +++ b/docs/documentation/generate/class.md @@ -1,10 +1,13 @@ - +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate class ## Overview `ng generate class [name]` generates a class +## Alias +cl - `ng generate cl [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/component.md b/docs/documentation/generate/component.md index 95335f9c792e..3e1e4dca8fda 100644 --- a/docs/documentation/generate/component.md +++ b/docs/documentation/generate/component.md @@ -1,10 +1,15 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate component ## Overview `ng generate component [name]` generates a component +## Alias +c - `ng generate c [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/directive.md b/docs/documentation/generate/directive.md index aa1653ebf9cc..94d7bc71783d 100644 --- a/docs/documentation/generate/directive.md +++ b/docs/documentation/generate/directive.md @@ -1,10 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate directive ## Overview `ng generate directive [name]` generates a directive +## Alias +d - `ng generate d [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/enum.md b/docs/documentation/generate/enum.md index b5614375d60b..82733a176401 100644 --- a/docs/documentation/generate/enum.md +++ b/docs/documentation/generate/enum.md @@ -1,10 +1,13 @@ - +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate enum ## Overview `ng generate enum [name]` generates an enumeration +## Alias +e - `ng generate e [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/guard.md b/docs/documentation/generate/guard.md index ea2e17aec000..5dd19b883a31 100644 --- a/docs/documentation/generate/guard.md +++ b/docs/documentation/generate/guard.md @@ -1,8 +1,13 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate guard ## Overview `ng generate guard [name]` generates a guard +## Alias +g - `ng generate g [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/interface.md b/docs/documentation/generate/interface.md index c769635e12b0..bd749ebd2d52 100644 --- a/docs/documentation/generate/interface.md +++ b/docs/documentation/generate/interface.md @@ -1,10 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate interface ## Overview `ng generate interface [name] ` generates an interface +## Alias +i - `ng generate i [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/library.md b/docs/documentation/generate/library.md index 2d62e7985834..48f4d0f40614 100644 --- a/docs/documentation/generate/library.md +++ b/docs/documentation/generate/library.md @@ -1,9 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. + # ng generate library ## Overview -Generate a library project for Angular. +`ng generate library [name]` generates a library project for Angular. + +## Alias +lib - `ng generate lib [name]` ## Options
@@ -57,7 +62,7 @@ Generate a library project for Angular. --skip-install

- Do not add dependencies to package.json. + Skip installing dependency packages.

diff --git a/docs/documentation/generate/module.md b/docs/documentation/generate/module.md index 090a04366098..25055ee35565 100644 --- a/docs/documentation/generate/module.md +++ b/docs/documentation/generate/module.md @@ -1,10 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate module ## Overview `ng generate module [name]` generates an NgModule +## Alias +m - `ng generate m [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/pipe.md b/docs/documentation/generate/pipe.md index e5384f787a19..cebbc9ce3c3e 100644 --- a/docs/documentation/generate/pipe.md +++ b/docs/documentation/generate/pipe.md @@ -1,10 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate pipe ## Overview `ng generate pipe [name]` generates a pipe +## Alias +p - `ng generate p [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/service.md b/docs/documentation/generate/service.md index e4008eb0fc20..c3da41708494 100644 --- a/docs/documentation/generate/service.md +++ b/docs/documentation/generate/service.md @@ -1,10 +1,14 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate service ## Overview `ng generate service [name]` generates a service +## Alias +s - `ng generate s [name]` + ## Options
dry-run diff --git a/docs/documentation/generate/universal.md b/docs/documentation/generate/universal.md index 945eaa3c2c0c..053689b12151 100644 --- a/docs/documentation/generate/universal.md +++ b/docs/documentation/generate/universal.md @@ -1,9 +1,10 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/generate)**. # ng generate universal ## Overview -Create an Angular universal app. +`ng generate universal [name]` creates an Angular universal app. ## Options
diff --git a/docs/documentation/help.md b/docs/documentation/help.md index 577f2cab8f9b..98f898764364 100644 --- a/docs/documentation/help.md +++ b/docs/documentation/help.md @@ -1,4 +1,6 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/help)**. + # ng help diff --git a/docs/documentation/home.md b/docs/documentation/home.md index 83762c301ab0..f8d2bb2a87ac 100644 --- a/docs/documentation/home.md +++ b/docs/documentation/home.md @@ -2,7 +2,7 @@ # Angular CLI -NOTE: this documentation is for Angular CLI 6. For Angular CLI 1.x go [here](1-x/home) instead. +**NOTE: this documentation is for Angular CLI 6. For Angular CLI Version 7 go [here](https://angular.io/cli) and 1.x go [here](1-x-home) instead.** ### Overview The Angular CLI is a tool to initialize, develop, scaffold and maintain [Angular](https://angular.io) applications @@ -13,6 +13,10 @@ To install the Angular CLI: npm install -g @angular/cli ``` +> If you get an error installing the CLI, this is an issue with your local npm setup on your machine, not a problem in Angular CLI. +> Please have a look at the [fixing npm permissions page](https://docs.npmjs.com/getting-started/fixing-npm-permissions), [common errors page](https://docs.npmjs.com/troubleshooting/common-errors), [npm issue tracker](https://github.com/npm/npm/issues), or open a new issue if the problem you are experiencing isn't known. +> To install a different version, see below. + Generating and serving an Angular project via a development server [Create](new) and [run](serve) a new project: ``` @@ -63,3 +67,12 @@ End-to-end tests are run via [Protractor](http://www.protractortest.org/). ### Additional Information There are several [stories](stories) which will walk you through setting up additional aspects of Angular applications. + +### Installing a specific version +The CLI is installed both globally (the command above with the `-g` argument to `npm install`) and also within the project. To install a different version of the CLI, you can just update the version locally within your project. The globally installed package will always delegate to the local one. + +There are several different versions available at any time: +- Install a previous version, maybe because of a bug in the latest version. For example to get 6.0.2: `npm install @angular/cli@6.0.2` +- Install the pre-release of a newer minor/major version, to try new features. For example to get 7.0.0-beta.3: `npm install @angular/cli@next`. Note that the `@next` version is only useful during beta periods. +- Install a snapshot build from the latest passing run of our CI (angular-cli/master). This is useful if an issue was just fixed or a new feature just landed on master, but is not yet released: `npm install @angular/cli@github:angular/cli-builds` (or maybe better, find the particular SHA that you want to try: +`npm install @angular/cli@github:angular/cli-builds#0123456789abcdef`) diff --git a/docs/documentation/lint.md b/docs/documentation/lint.md index 46cf98642821..5418a991d338 100644 --- a/docs/documentation/lint.md +++ b/docs/documentation/lint.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/lint)**. # ng lint diff --git a/docs/documentation/new.md b/docs/documentation/new.md index 4e7ebd260918..8ed2220272b4 100644 --- a/docs/documentation/new.md +++ b/docs/documentation/new.md @@ -1,5 +1,7 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/new)**. + # ng new ## Overview diff --git a/docs/documentation/run.md b/docs/documentation/run.md index c7de54d15085..f5b107733158 100644 --- a/docs/documentation/run.md +++ b/docs/documentation/run.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/run)**. # ng run diff --git a/docs/documentation/serve.md b/docs/documentation/serve.md index b064c71ab3f5..1a7ba7ba0bc2 100644 --- a/docs/documentation/serve.md +++ b/docs/documentation/serve.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/serve)**. # ng serve diff --git a/docs/documentation/stories.md b/docs/documentation/stories.md index 3d21cda0a10c..b32673dcf112 100644 --- a/docs/documentation/stories.md +++ b/docs/documentation/stories.md @@ -5,7 +5,6 @@ - [1.0 Update](stories/1.0-update) - [App Shell](stories/app-shell) - [Asset Configuration](stories/asset-configuration) - - [Autocompletion](stories/autocompletion) - [Configure Hot Module Replacement](stories/configure-hmr) - [CSS Preprocessors](stories/css-preprocessors) - [Global Lib](stories/global-lib) diff --git a/docs/documentation/stories/1.0-update.md b/docs/documentation/stories/1.0-update.md new file mode 100644 index 000000000000..b3c1f8fde90b --- /dev/null +++ b/docs/documentation/stories/1.0-update.md @@ -0,0 +1,505 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available in [angular.io](https://angular.io)**. + +# Angular CLI migration guide + +In this migration guide we'll be looking at some of the major changes to CLI projects in the +last two months. + +Most of these changes were not breaking changes and your project should work fine without them. + +But if you've been waiting for the perfect time to update, this is it! +Along with major rebuild speed increases, we've been busy adding a lot of features. + +Documentation has also completely moved to [the wiki](https://github.com/angular/angular-cli/wiki). +The new [Stories](https://github.com/angular/angular-cli/wiki/stories) section covers common usage +scenarios, so be sure to have a look! + +Below are the changes between a project generated two months ago, with `1.0.0-beta.24` and +a `1.0.0` project. +If you kept your project up to date you might have a lot of these already. + +You can find more details about changes between versions in [the releases tab on GitHub](https://github.com/angular/angular-cli/releases). + +If you prefer, you can also generate a new project in a separate folder using + `ng new upgrade-project --skip-install` and compare the differences. + +## @angular/cli + +Angular CLI can now be found on NPM under `@angular/cli` instead of `angular-cli`, and upgrading is a simple 3 step process: + +1. Uninstall old version +2. Update node/npm if necessary +3. Install new version + +### 1. Uninstall old version + +If you're using Angular CLI `beta.28` or less, you need to uninstall the `angular-cli` packages: +```bash +npm uninstall -g angular-cli # Remove global package +npm uninstall --save-dev angular-cli # Remove from package.json +``` + +Otherwise, uninstall the `@angular/cli` packages: +```bash +npm uninstall -g @angular/cli # Remove global package +npm uninstall --save-dev @angular/cli # Remove from package.json +``` + +Also purge the cache and local packages: +``` +rm -rf node_modules dist # Use rmdir on Windows +npm cache clean +``` + +At this point, you should not have Angular CLI on your system anymore. If invoking Angular CLI at the commandline reveals that it still exists on your system, you will have to manually remove it. See _Manually removing residual Angular CLI_. + +### 2. Update node/npm if necessary + +Angular CLI now has a minimum requirement of Node 6.9.0 or higher, together with NPM 3 or higher. + +If your Node or NPM versions do not meet these requirements, please refer to [the documentation](https://docs.npmjs.com/getting-started/installing-node) on how to upgrade. + +### 3. Install the new version + +To update Angular CLI to a new version, you must update both the global package and your project's local package: + +```bash +npm install -g @angular/cli@latest # Global package +npm install --save-dev @angular/cli@latest # Local package +npm install # Restore removed dependencies +``` + +### Manually removing residual Angular CLI + +If you accidentally updated NPM before removing the old Angular CLI, you may find that uninstalling the package using `npm uninstall` is proving fruitless. This _could_ be because the global install (and uninstall) path changed between versions of npm from `/usr/local/lib` to `/usr/lib`, and hence, no longer searches through the old directory. In this case you'll have to remove it manually: + +`rm -rf /usr/local/lib/node_modules/@angular/cli` + +If the old Angular CLI package _still_ persists, you'll need to research how to remove it before proceeding with the upgrade. + +## .angular-cli.json + +`angular-cli.json` is now `angular.json`, but we still accept the old config file name. + +A few new properties have changed in it: + +### Schema + +Add the `$schema` property above project for handy IDE support on your config file: + +``` +"$schema": "./node_modules/@angular/cli/lib/config/schema.json", +``` + +### Polyfills + +There is now a dedicated entry for polyfills ([#3812](https://github.com/angular/angular-cli/pull/3812)) +inside `apps[0].polyfills`, between `main` and `test`: + +``` +"main": "main.ts", +"polyfills": "polyfills.ts", +"test": "test.ts", +``` + +Add it and remove `import './polyfills.ts';` from `src/main.ts` and `src/test.ts`. + +We also added a lot of descriptive comments to the existing `src/polyfills.ts` file, explaining +which polyfills are needed for what browsers. +Be sure to check it out in a new project! + +### Environments + +A new `environmentSource` entry ([#4705](https://github.com/angular/angular-cli/pull/4705)) +replaces the previous source entry inside environments. + +To migrate angular-cli.json follow the example below: + +Before: +``` +"environments": { + "source": "environments/environment.ts", + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" +} +``` + +After: + +``` +"environmentSource": "environments/environment.ts", +"environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" +} +``` + +### Linting + +The CLI now uses the TSLint API ([#4248](https://github.com/angular/angular-cli/pull/4248)) +to lint several TS projects at once. + +There is a new `lint` entry in `.angular-cli.json` between `e2e` and `test` where all linting +targets are listed: + +``` +"e2e": { + "protractor": { + "config": "./protractor.conf.js" + } +}, +"lint": [ + { + "project": "src/tsconfig.app.json" + }, + { + "project": "src/tsconfig.spec.json" + }, + { + "project": "e2e/tsconfig.e2e.json" + } +], +"test": { + "karma": { + "config": "./karma.conf.js" + } +}, +``` + +### Generator defaults + +Now you can list generator defaults per generator ([#4389](https://github.com/angular/angular-cli/pull/4389)) +in `defaults`. + +Instead of: +``` +"defaults": { + "styleExt": "css", + "prefixInterfaces": false, + "inline": { + "style": false, + "template": false + }, + "spec": { + "class": false, + "component": true, + "directive": true, + "module": false, + "pipe": true, + "service": true + } +} +``` + +You can instead list the flags as they appear on [the generator command](https://github.com/angular/angular-cli/wiki/generate-component): +``` +"defaults": { + "styleExt": "css", + "component": { + "inlineTemplate": false, + "spec": true + } +} +``` + +## One tsconfig per app + +CLI projects now use one tsconfig per app ([#4924](https://github.com/angular/angular-cli/pull/4924)). + +- `src/tsconfig.app.json`: configuration for the Angular app. +``` +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "lib": [ + "es2017", + "dom" + ], + "outDir": "../out-tsc/app", + "module": "es2015", + "baseUrl": "", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} +``` +- `src/tsconfig.spec.json`: configuration for the unit tests. +``` +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2017", + "dom" + ], + "outDir": "../out-tsc/spec", + "module": "commonjs", + "target": "es5", + "baseUrl": "", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} + +``` +- `e2e/tsconfig.e2e.json`: configuration for the e2e tests. +``` +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2017" + ], + "outDir": "../out-tsc/e2e", + "module": "commonjs", + "target": "es5", + "types":[ + "jasmine", + "node" + ] + } +} + +``` + +There is an additional root-level `tsconfig.json` that is used for IDE integration. +``` +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "baseUrl": "src", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} +``` + +You can delete `e2e/tsconfig.json` and `src/tsconfig.json` after adding these. + +Also update `.angular-cli.json` to use them inside `apps[0]`: + +``` +"tsconfig": "tsconfig.app.json", +"testTsconfig": "tsconfig.spec.json", +``` + +Then update `protractor.conf.js` to use the e2e config as well: +``` +beforeLaunch: function() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); +}, +``` + +These configs have an `types` array where you should add any package you install via `@types/*`. +This array helps keep typings isolated to the apps that really need them and avoid problems with +duplicate typings. + +For instance, the unit test `tsconfig` has `jasmine` and `node`, which correspond to +`@types/jasmine` and `@types/node`. +Add any typings you've installed to the appropriate `tsconfig` as well. + +## typings.d.ts + +There's a new `src/typings.d.ts` file that serves two purposes: +- provides a centralized place for users to add their own custom typings +- makes it easy to use components that use `module.id`, present in the documentation and in snippets + +``` +/* SystemJS module definition */ +declare var module: NodeModule; +interface NodeModule { + id: string; +} +``` + +## package.json + +We've updated a lot of packages over the last months in order to keep projects up to date. + +Additions or removals are found in bold below. + +Packages in `dependencies`: +- `@angular/*` packages now have a `^4.0.0` minimum +- `core-js` remains unchanged at `^2.4.1` +- `rxjs` was updated to `^5.1.0` +- `ts-helpers` was **removed** +- `zone.js` was updated to `^0.8.4` + +Packages in `devDependencies`: +- `@angular/cli` at `1.0.0` replaces `angular-cli` +- `@angular/compiler-cli` is also at `^4.0.0` +- `@types/jasmine` remains unchanged and pinned at `2.5.38` +- `@types/node` was updated to `~6.0.60` +- `codelyzer` was updated to `~2.0.0` +- `jasmine-core` was updated to `~2.5.2` +- `jasmine-spec-reporter` was updated to `~3.2.0` +- `karma` was updated to `~1.4.1` +- `karma-chrome-launcher` was updated to `~2.0.0` +- `karma-cli` was updated to `~1.0.1` +- `karma-jasmine` was updated to `~1.1.0` +- `karma-jasmine-html-reporter` was **added** at `^0.2.2` +- `karma-coverage-istanbul-reporter` was **added** at `^0.2.0`, replacing `karma-remap-istanbul` +- `karma-remap-istanbul` was **removed** +- `protractor` was updated to `~5.1.0` +- `ts-node` was updated to `~2.0.0` +- `tslint` was updated to `~4.5.0` +- `typescript` was updated to `~2.1.0` + +See the [karma](#karma.conf.js) and [protractor](#protractor.conf.js) sections below for more +information on changed packages. + +The [Linting rules](#Linting rules) section contains a list of rules that changed due to updates. + +We also updated the scripts section to make it more simple: + +``` +"scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" +}, +``` + +## karma.conf.js + +Karma configuration suffered some changes to improve the code-coverage functionality, +use the new `@angular/cli` package, and the new HTML reporter. + +In the `frameworks` array update the CLI package to `@angular/cli`. + +In the `plugins` array: +- add `require('karma-jasmine-html-reporter')` and `require('karma-coverage-istanbul-reporter')` +- remove `require('karma-remap-istanbul')` +- update the CLI plugin to `require('@angular/cli/plugins/karma')` + +Add a new `client` option just above `patterns`: +``` +client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser +}, +files: [ +``` + +Change the preprocessor to use the new CLI package: +``` +preprocessors: { + './src/test.ts': ['@angular/cli'] +}, +``` + +Replace `remapIstanbulReporter` with `coverageIstanbulReporter`: +``` +coverageIstanbulReporter: { + reports: [ 'html', 'lcovonly' ], + fixWebpackSourcePaths: true +}, +``` + +Remove the config entry from `angularCli`: +``` +angularCli: { + environment: 'dev' +}, +``` + +Update the reporters to use `coverage-istanbul` instead of `karma-remap-istanbul`, and +add `kjhtml` (short for karma-jasmine-html-reporter): +``` +reporters: config.angularCli && config.angularCli.codeCoverage + ? ['progress', 'coverage-istanbul'] + : ['progress', 'kjhtml'], +``` + +## protractor.conf.js + +Protractor was updated to the new 5.x major version, but you shouldn't need to change much +to take advantage of all its new features. + +Replace the spec reporter import from: +``` +var SpecReporter = require('jasmine-spec-reporter'); +``` +to +``` +const { SpecReporter } = require('jasmine-spec-reporter'); +``` + +Remove `useAllAngular2AppRoots: true`. + +Update `beforeLaunch` as described in [One tsconfig per app](#one-tsconfig-per-app): +``` +beforeLaunch: function() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); +}, +``` + +Update `onPrepare`: +``` +onPrepare: function() { + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); +} +``` + +## Linting rules + +The updated versions of `tslint` and `codelyzer` contain a few rule changes that you should +apply to your `tslint.json`: + +Add these new rules: +``` +"callable-types": true, +"import-blacklist": [true, "rxjs"], +"import-spacing": true, +"interface-over-type-literal": true, +"no-empty-interface": true, +"no-string-throw": true, +"prefer-const": true, +"typeof-compare": true, +"unified-signatures": true, +``` + +Update `no-inferrable-types` to `"no-inferrable-types": [true, "ignore-params"]`. diff --git a/docs/documentation/stories/app-shell.md b/docs/documentation/stories/app-shell.md index 2428f65b2f65..66c27058bb31 100644 --- a/docs/documentation/stories/app-shell.md +++ b/docs/documentation/stories/app-shell.md @@ -1,4 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available in [angular.io](https://angular.io)**. # App shell @@ -48,7 +49,12 @@ build with the app shell target ``` ng run my-app:app-shell ``` +If you would like to build for production you should run the following command +``` +ng run my-app:app-shell:production +ng run my-app:app-shell --configuration production +``` Verify the build output -Open dist/app-shell/index.html +Open dist/index.html look for text "app-shell works!" which verifies that the app shell route was rendered as part of the output diff --git a/docs/documentation/stories/application-environments.md b/docs/documentation/stories/application-environments.md index 635bc2d39fe6..f263ec36419e 100644 --- a/docs/documentation/stories/application-environments.md +++ b/docs/documentation/stories/application-environments.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/build#configuring-application-environments)**. + # Application Environments In Angular CLI you can configure the build system to replace existing files for your intended diff --git a/docs/documentation/stories/asset-configuration.md b/docs/documentation/stories/asset-configuration.md index 6d2f50556cd9..c19602ff18a2 100644 --- a/docs/documentation/stories/asset-configuration.md +++ b/docs/documentation/stories/asset-configuration.md @@ -1,7 +1,10 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation is available [here](https://angular.io/guide/build)**. + # Project assets You use the `assets` array inside the build target in `angular.json` to list files or folders -you want to copy as-is when building your project. +you want to copy as-is when building your project. if you think you need to exclude files, +consider not putting that thing in the assets By default, the `src/assets/` folder and `src/favicon.ico` are copied over. @@ -19,13 +22,14 @@ The array below does the same as the default one: ```json "assets": [ { "glob": "**/*", "input": "src/assets/", "output": "/assets/" }, - { "glob": "favicon.ico", "input": "/src", "output": "/" }, + { "glob": "favicon.ico", "input": "src/", "output": "/" }, ] ``` -`glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. -`input` is relative to the workspace root, while `output` is relative to `outDir` -(`dist/project-name` default). +- `glob` is a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. +- `input` is relative to the workspace root. +- `ignore` is a list of globs to ignore from copying. +- `output` is relative to `outDir` (`dist/project-name` default). You can use this extended configuration to copy assets from outside your project. For instance, you can copy assets from a node package: @@ -36,7 +40,14 @@ The array below does the same as the default one: ] ``` -The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`. +You can ignore certain files from copying by using the `ignore` option: + ```json +"assets": [ + { "glob": "**/*", "input": "src/assets/", "ignore": ["**/*.svg"], "output": "/assets/" }, +] +``` + +The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`. ## Writing assets outside of `dist/` diff --git a/docs/documentation/stories/autoprefixer.md b/docs/documentation/stories/autoprefixer.md index c6902fccaf3b..7c3e0a202e11 100644 --- a/docs/documentation/stories/autoprefixer.md +++ b/docs/documentation/stories/autoprefixer.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation is available [here](https://angular.io/guide/build#configuring-browser-compatibility)**. + # Change target browsers for Autoprefixer Currently, the CLI uses [Autoprefixer](https://github.com/postcss/autoprefixer) to ensure compatibility diff --git a/docs/documentation/stories/budgets.md b/docs/documentation/stories/budgets.md index f6eea0f0311a..726f352e8a08 100644 --- a/docs/documentation/stories/budgets.md +++ b/docs/documentation/stories/budgets.md @@ -1,8 +1,10 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/build#configure-size-budgets)**. + # Budgets As applications grow in functionality, they also grow in size. Budgets is a feature in the Angular CLI which allows you to set budget thresholds in your configuration to ensure parts -of your application stay within boundries which you set. +of your application stay within boundaries which you set. **angular.json** ``` @@ -60,3 +62,24 @@ Available formats: All sizes are relative to baseline. Percentages are not valid for baseline values. + +## Example + +``` +{ + ... + "configurations": { + "production": { + ... + budgets: [ + { + "type": "bundle", + "name": "vendor", + "minimumWarning": "300kb", + "minimumError": "400kb", + } + ] + } + } +} +``` diff --git a/docs/documentation/stories/code-coverage.md b/docs/documentation/stories/code-coverage.md index 15f67bb317da..da623b42ff1d 100644 --- a/docs/documentation/stories/code-coverage.md +++ b/docs/documentation/stories/code-coverage.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/testing#enable-code-coverage-reports)**. + # Code Coverage With the Angular CLI we can run unit tests as well as create code coverage reports. Code coverage reports allow us to see any parts of our code base that may not be properly tested by our unit tests. diff --git a/docs/documentation/stories/configure-hmr.md b/docs/documentation/stories/configure-hmr.md index 58e34ac9245b..b3520a67a271 100644 --- a/docs/documentation/stories/configure-hmr.md +++ b/docs/documentation/stories/configure-hmr.md @@ -1,7 +1,9 @@ +**HMR is a webpack feature that is not officially supported by Angular. Community contributors can submit PRs against this page to add corrections, new information, or advice.**. + # Configure Hot Module Replacement -Hot Module Replacement (HMR) is a WebPack feature to update code in a running app without rebuilding it. -This results in faster updates and less full page-reloads. +Hot Module Replacement (HMR) is a [webpack](https://webpack.js.org) feature to update code in a running app without rebuilding it. +This results in faster updates and fewer full page-reloads. You can read more about HMR by visiting [this page](https://webpack.js.org/guides/hot-module-replacement/). @@ -42,7 +44,7 @@ export const environment = { Update `angular.json` to include an hmr environment as explained [here](./application-environments) -and add configurations within build and serve to enable hmr. Note that `` here +and add configurations within build and serve to enable hmr. Note that `` here represents the name of the project you are adding this configuration to in `angular.json`. ```json @@ -176,6 +178,4 @@ When starting the server Webpack will tell you that it’s enabled: NOTICE Hot Module Replacement (HMR) is enabled for the dev server. -Now if you make changes to one of your components they changes should be visible automatically without a complete browser refresh. - - +Now if you make changes to one of your components the changes should be visible automatically without a complete browser refresh. diff --git a/docs/documentation/stories/continuous-integration.md b/docs/documentation/stories/continuous-integration.md index 269bab08997c..afecbd429674 100644 --- a/docs/documentation/stories/continuous-integration.md +++ b/docs/documentation/stories/continuous-integration.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/testing#set-up-continuous-integration)**. + # Continuous Integration One of the best ways to keep your project bug free is through a test suite, but it's easy to forget diff --git a/docs/documentation/stories/create-library.md b/docs/documentation/stories/create-library.md index 8ccf97421c49..f4be4e7a3dc5 100644 --- a/docs/documentation/stories/create-library.md +++ b/docs/documentation/stories/create-library.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/creating-libs)**. + # Library support in Angular CLI 6 Angular CLI v6 comes with library support via [ng-packagr](https://github.com/dherges/ng-packagr) @@ -62,7 +64,7 @@ If this happens just rebuild your library. To publish your library follow these steps: ``` -ng build my-lib --prod +ng build my-lib cd dist/my-lib npm publish ``` @@ -71,9 +73,6 @@ If you've never published a package in npm before, you will need to create a use You can read more about publishing on npm here: https://docs.npmjs.com/getting-started/publishing-npm-packages -The `--prod` flag should be used when building to publish because it will completely clean the build -directory for the library beforehand, removing old code leftover code from previous versions. - ## Why do I need to build the library everytime I make changes? diff --git a/docs/documentation/stories/css-preprocessors.md b/docs/documentation/stories/css-preprocessors.md index 90d93282d498..35e7441ffa7a 100644 --- a/docs/documentation/stories/css-preprocessors.md +++ b/docs/documentation/stories/css-preprocessors.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available here [angular.io](https://angular.io/guide/build)**. + # CSS Preprocessor integration Angular CLI supports all major CSS preprocessors: diff --git a/docs/documentation/stories/disk-serve.md b/docs/documentation/stories/disk-serve.md index b3f4663b630b..3d7be396125b 100644 --- a/docs/documentation/stories/disk-serve.md +++ b/docs/documentation/stories/disk-serve.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available here [angular.io](https://angular.io/guide/build)**. + # Serve from Disk The CLI supports running a live browser reload experience to users by running `ng serve`. This will compile the application upon file saves and reload the browser with the newly compiled application. This is done by hosting the application in memory and serving it via [webpack-dev-server](https://webpack.js.org/guides/development/#webpack-dev-server). diff --git a/docs/documentation/stories/github-pages.md b/docs/documentation/stories/github-pages.md index 658c8340a863..a41b375c7e99 100644 --- a/docs/documentation/stories/github-pages.md +++ b/docs/documentation/stories/github-pages.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/deployment#deploy-to-github-pages)**. + # Deploy to GitHub Pages A simple way to deploy your Angular app is to use diff --git a/docs/documentation/stories/global-lib.md b/docs/documentation/stories/global-lib.md index 74f1f3a64587..d82089140668 100644 --- a/docs/documentation/stories/global-lib.md +++ b/docs/documentation/stories/global-lib.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available here [angular.io](https://angular.io/guide/build)**. + # Global Library Installation Some javascript libraries need to be added to the global scope and loaded as if diff --git a/docs/documentation/stories/global-scripts.md b/docs/documentation/stories/global-scripts.md index c67a052629f3..c0cecbd8b809 100644 --- a/docs/documentation/stories/global-scripts.md +++ b/docs/documentation/stories/global-scripts.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available here [angular.io](https://angular.io/guide/build)**. + # Global scripts You can add Javascript files to the global scope via the `scripts` option inside your diff --git a/docs/documentation/stories/global-styles.md b/docs/documentation/stories/global-styles.md index 32ff05b4cd23..300a7e953837 100644 --- a/docs/documentation/stories/global-styles.md +++ b/docs/documentation/stories/global-styles.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation will be available here [angular.io](https://angular.io/guide/build)**. + # Global styles The `styles.css` file allows users to add global styles and supports diff --git a/docs/documentation/stories/include-angular-flex.md b/docs/documentation/stories/include-angular-flex.md index 107db0e9c1c9..fa381baaacb1 100644 --- a/docs/documentation/stories/include-angular-flex.md +++ b/docs/documentation/stories/include-angular-flex.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this.** + ## Include [Flex Layout](https://github.com/angular/flex-layout) in your CLI application diff --git a/docs/documentation/stories/include-angular-material.md b/docs/documentation/stories/include-angular-material.md index e4e36bc015d5..9c6a440a051b 100644 --- a/docs/documentation/stories/include-angular-material.md +++ b/docs/documentation/stories/include-angular-material.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this.** + # Include [Angular Material](https://material.angular.io) [Angular Material](https://material.angular.io) is a set of Material Design components for Angular apps. To get started please visit these links to the Angular Material project: diff --git a/docs/documentation/stories/include-angularfire.md b/docs/documentation/stories/include-angularfire.md index a99f15caae3a..9c3ab4ba3eaf 100644 --- a/docs/documentation/stories/include-angularfire.md +++ b/docs/documentation/stories/include-angularfire.md @@ -1,4 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this.** # Include AngularFire diff --git a/docs/documentation/stories/include-bootstrap.md b/docs/documentation/stories/include-bootstrap.md index 82acb4dc4dae..f4efc8169996 100644 --- a/docs/documentation/stories/include-bootstrap.md +++ b/docs/documentation/stories/include-bootstrap.md @@ -1,4 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this.** # Include [Bootstrap](http://getbootstrap.com/) diff --git a/docs/documentation/stories/include-font-awesome.md b/docs/documentation/stories/include-font-awesome.md index dad47e6d2d7b..93036b3461cc 100644 --- a/docs/documentation/stories/include-font-awesome.md +++ b/docs/documentation/stories/include-font-awesome.md @@ -1,4 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this.** # Include [Font Awesome](https://fontawesome.com/) @@ -23,13 +24,31 @@ To add Font Awesome CSS icons to your app... "build": { "options": { "styles": [ - "../node_modules/font-awesome/css/font-awesome.css" - "styles.css" + "../node_modules/font-awesome/css/font-awesome.css", + "src/styles.css" ], } } ``` ### Using SASS +Create new project with SASS: +``` +ng new cli-app --style=scss +``` +To use with existing project with `CSS`: +1. Rename `src/styles.css` to `src/styles.scss` +2. Change `angular.json` to look for `styles.scss` instead of css: +``` +// in angular.json +"build": { + "options": { + "styles": [ + "src/styles.scss" + ], + } +} +``` +Make sure to change `styles.css` to `styles.scss`. Create an empty file _variables.scss in src/. diff --git a/docs/documentation/stories/internationalization.md b/docs/documentation/stories/internationalization.md index 2acd551752ba..d71b8bfef2ac 100644 --- a/docs/documentation/stories/internationalization.md +++ b/docs/documentation/stories/internationalization.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/i18n)**. + # Internationalization (i18n) If you are working on internationalization, the CLI can help you with the following steps: diff --git a/docs/documentation/stories/multiple-projects.md b/docs/documentation/stories/multiple-projects.md index d1a49bce61e4..79743c8b57db 100644 --- a/docs/documentation/stories/multiple-projects.md +++ b/docs/documentation/stories/multiple-projects.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation is available [here](https://angular.io/guide/build)**. + # Multiple Projects Angular CLI supports multiple applications within one workspace. diff --git a/docs/documentation/stories/proxy.md b/docs/documentation/stories/proxy.md index c0cfd0e1cabc..5ce38cb24b93 100644 --- a/docs/documentation/stories/proxy.md +++ b/docs/documentation/stories/proxy.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/build#using-corporate-proxy)**. + # Proxy To Backend Using the [proxying support](https://webpack.js.org/configuration/dev-server/#devserver-proxy) in webpack's dev server we can highjack certain URLs and send them to a backend server. @@ -26,7 +28,7 @@ We can then add the `proxyConfig` option to the serve target: "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "your-application-name:build", - "proxyConfig": "src/proxy.conf.json" + "proxyConfig": "proxy.conf.json" }, ``` @@ -119,7 +121,7 @@ Make sure to point to the right file (`.js` instead of `.json`): "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "your-application-name:build", - "proxyConfig": "src/proxy.conf.js" + "proxyConfig": "proxy.conf.js" }, ``` @@ -183,4 +185,4 @@ function setupForCorporateProxy(proxyConfig) { module.exports = setupForCorporateProxy(proxyConfig); ``` -This way if you have a `http_proxy` or `HTTP_PROXY` environment variable defined, an agent will automatically be added to pass calls through your corporate proxy when running `npm start`. \ No newline at end of file +This way if you have a `http_proxy` or `HTTP_PROXY` environment variable defined, an agent will automatically be added to pass calls through your corporate proxy when running `npm start`. diff --git a/docs/documentation/stories/routing.md b/docs/documentation/stories/routing.md index 504a62e6f07e..023f8a43461c 100644 --- a/docs/documentation/stories/routing.md +++ b/docs/documentation/stories/routing.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/router#integrate-routing-with-your-app)**. + # Generating a route The CLI supports routing in several ways: diff --git a/docs/documentation/stories/third-party-lib.md b/docs/documentation/stories/third-party-lib.md index 272635865808..bba00af34ad2 100644 --- a/docs/documentation/stories/third-party-lib.md +++ b/docs/documentation/stories/third-party-lib.md @@ -1,3 +1,5 @@ +**Documentation below is deprecated and we no longer accept PRs to improve this. The new documentation is available [here](https://angular.io/guide/build)**. + # 3rd Party Library Installation Simply install your library via `npm install lib-name --save` and import it in your code. diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index cb007b6eb712..f05c5769f640 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -1,3 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/guide/universal)**. + # Angular Universal Integration The Angular CLI supports generation of a Universal build for your application. This is a CommonJS-formatted bundle which can be `require()`'d into a Node application (for example, an Express server) and used with `@angular/platform-server`'s APIs to prerender your application. @@ -20,11 +22,10 @@ This story will show you how to set up Universal bundling for an existing `@angu Install `@angular/platform-server` into your project. Make sure you use the same version as the other `@angular` packages in your project. -> You'll also need ts-loader (for your webpack build we'll show later) and @nguniversal/module-map-ngfactory-loader, as it's used to handle lazy-loading in the context of a server-render. (by loading the chunks right away) +> You'll also need @nguniversal/module-map-ngfactory-loader, as it's used to handle lazy-loading in the context of a server-render. (by loading the chunks right away) ```bash $ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader express -$ npm install --save-dev ts-loader webpack-cli ``` ## Step 1: Prepare your App for Universal rendering @@ -197,6 +198,30 @@ app.engine('html', ngExpressEngine({ })); ``` +In case you want to use an AppShell with SSR and Lazy loaded modules you need to configure `NgModuleFactoryLoader` as a provider. + +For more context see: https://github.com/angular/angular-cli/issues/9202 + +```typescript +import { Compiler, NgModuleFactoryLoader } from '@angular/core'; +import { provideModuleMap, ModuleMapNgFactoryLoader, MODULE_MAP } from '@nguniversal/module-map-ngfactory-loader'; + +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP), + { + provide: NgModuleFactoryLoader, + useClass: ModuleMapNgFactoryLoader, + deps: [ + Compiler, + MODULE_MAP + ], + }, + ] +})); +``` + Below we can see a TypeScript implementation of a -very- simple Express server to fire everything up. > Note: This is a very bare bones Express application, and is just for demonstrations sake. In a real production environment, you'd want to make sure you have other authentication and security things setup here as well. This is just meant just to show the specific things needed that are relevant to Universal itself. The rest is up to you! @@ -263,60 +288,36 @@ app.listen(PORT, () => { }); ``` -## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application! +## Step 5: Setup a TypeScript config to handle this Node server.ts file and serve your application! Now that we have our Node Express server setup, we need to pack it and serve it! -Create a file named `webpack.server.config.js` at the ROOT of your application. +Create a file named `server.tsconfig.json` at the ROOT of your application. > This file basically takes that server.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`. -### ./webpack.server.config.js (root project level) +### ./server.tsconfig.json (root project level) ```typescript -const path = require('path'); -const webpack = require('webpack'); - -module.exports = { - mode: 'none', - entry: { - server: './server.ts', - }, - target: 'node', - resolve: { extensions: ['.ts', '.js'] }, - optimization: { - minimize: false - }, - output: { - // Puts the output at the root of the dist folder - path: path.join(__dirname, 'dist'), - filename: '[name].js' - }, - module: { - rules: [ - { test: /\.ts$/, loader: 'ts-loader' }, - { - // Mark files inside `@angular/core` as using SystemJS style dynamic imports. - // Removing this will cause deprecation warnings to appear. - test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, - parser: { system: true }, - }, +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" ] }, - plugins: [ - new webpack.ContextReplacementPlugin( - // fixes WARNING Critical dependency: the request of a dependency is an expression - /(.+)?angular(\\|\/)core(.+)?/, - path.join(__dirname, 'src'), // location of your src - {} // a map of your routes - ), - new webpack.ContextReplacementPlugin( - // fixes WARNING Critical dependency: the request of a dependency is an expression - /(.+)?express(\\|\/)(.+)?/, - path.join(__dirname, 'src'), - {} - ) - ] + "include": ["server.ts"] } ``` @@ -343,12 +344,12 @@ Now lets create a few handy scripts to help us do all of this in the future. ```json "scripts": { // These will be your common scripts - "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server", + "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", "serve:ssr": "node dist/server.js", // Helpers for the above scripts "build:client-and-server-bundles": "ng build --prod && ng run your-project-name:server", - "webpack:server": "webpack --config webpack.server.config.js --progress --colors" + "compile:server": "tsc -p server.tsconfig.json", } ``` diff --git a/docs/documentation/test.md b/docs/documentation/test.md index ea5c128225ea..5b00f3286c71 100644 --- a/docs/documentation/test.md +++ b/docs/documentation/test.md @@ -1,4 +1,6 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/test)**. + # ng test diff --git a/docs/documentation/update.md b/docs/documentation/update.md index 7d2cebe984fb..2a540e5693e9 100644 --- a/docs/documentation/update.md +++ b/docs/documentation/update.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/update)**. # ng update diff --git a/docs/documentation/version.md b/docs/documentation/version.md index c6418d66cff5..4e58dc803a6b 100644 --- a/docs/documentation/version.md +++ b/docs/documentation/version.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/version)**. # ng version diff --git a/docs/documentation/xi18n.md b/docs/documentation/xi18n.md index 835873787bef..01c00ca475f8 100644 --- a/docs/documentation/xi18n.md +++ b/docs/documentation/xi18n.md @@ -1,4 +1,5 @@ +**Documentation below is for CLI version 6 and we no longer accept PRs to improve this. For version 7 see [here](https://angular.io/cli/xi18n)**. # ng xi18n diff --git a/docs/process/bazel.md b/docs/process/bazel.md new file mode 100644 index 000000000000..01a63a1f0a67 --- /dev/null +++ b/docs/process/bazel.md @@ -0,0 +1,30 @@ +## Yarn Workspaces + +The package architecture of `angular-cli` repository is originally setup using +yarn [workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). This means the +dependencies of various `package.json` in the repository are linked and +installed together. + +## Bazel + +Since then, Bazel was introduced to manage some of the build dependencies in +`angular-cli` repo. However, Bazel does **not** yet support yarn workspaces, +since it requires laying out more than one `node_modules` directory. In this +mixed mode, developers ought to take extra care to synchronize the dependencies. + +Since the `yarn_install` rule that installs all NPM dependencies in this +repository only reads from the **root** `package.json`, every Bazel target that +depends on packages downloaded from the NPM registry will need to have the +dependency declared in the **root** `package.json`. + +In addition, if the dependency is also needed at runtime (non-dev dependencies), +the dependency on the individual package's `package.json` has to be updated as +well. This is to ensure that when users download a published version from NPM, +they will be able to install all dependencies correctly without Bazel. It is the +responsibility of the developer to keep both `package.json` in sync. + +## Issues + +1. Yarn workspaces is not compatible with Bazel-managed deps + [(#12736)](https://github.com/angular/angular-cli/issues/12736) + diff --git a/docs/process/release.md b/docs/process/release.md index 6ae887075057..da502f7dd095 100644 --- a/docs/process/release.md +++ b/docs/process/release.md @@ -41,15 +41,19 @@ Only the first 2 commits should be cherry picked to the patch branch, as the las # Release +## Before releasing + +Make sure the CI is green. + +Consider if you need to update `packages/schematics/angular/utility/latest-versions.ts` to reflect changes in dependent versions. + ## Shepparding -As commits are cherry-picked when PRs are merged, creating the release should be a matter of updating the version -numbers. This can be done with the following command. +As commits are cherry-picked when PRs are merged, creating the release should be a matter of creating a tag. -See `scripts/release.ts` for the full list of release types, e.g. patch updates the third number per semver. +**Make sure you update the package versions in `packages/schematics/angular/utility/latest-versions.ts`.** ```bash -devkit-admin release patch --force # replace with minor-beta etc. git commit -a -m 'release: vXX' git tag 'vXX' git push upstream && git push upstream --tags @@ -71,6 +75,24 @@ Check out the minor tag (e.g. `v6.8.0-beta.0`), then run: devkit-admin publish --tag next ``` +### Microsite Publishing + +**This can ONLY be done by a Google employee.** + +**You will need firebase access to our cli-angular-io firebase site. If you don't have it, escalate.** + +Check out if changes were made to the microsite: + +```sh +git log v8.0.0-beta.0..HEAD --oneline etc/cli.angular.io | wc -l +``` + +If the number is 0 you can ignore the rest of this section. + +To publish, go to the `etc/cli.angular.io` directory and run `firebase deploy`. You might have to `firebase login` first. If you don't have the firebase CLI installed, you can install it using `npm install --global firebase-tools` (or use your package manager of choice). + +This is detailed in `etc/cli.angular.io/README.md`. + ### Release Notes `devkit-admin changelog` takes `from` and `to` arguments which are any valid git ref. diff --git a/docs/specifications/schematic-prompts.md b/docs/specifications/schematic-prompts.md new file mode 100644 index 000000000000..08092ef20235 --- /dev/null +++ b/docs/specifications/schematic-prompts.md @@ -0,0 +1,121 @@ +# Schematic Prompts + +Schematic prompts provide the ability to introduce user interaction into the schematic execution. The schematic runtime supports the ability to allow schematic options to be configured to display a customizable question to the user and then use the response as the value for the option. These prompts are displayed before the execution of the schematic. This allows users direct the operation of the schematic without requiring indepth knowledge of the full spectrum of options available to the user. + +To enable this capability, the JSON Schema used to define the schematic's options supports extensions to allow the declarative definition of the prompts and their respective behavior. No additional logic or changes are required to the JavaScript for a schematic to support the prompts. + +## Basic Usage + +To illustrate the addition of a prompt to an existing schematic the following example JSON schema for a hypothetical _hello world_ schematic will be used. + +```json +{ + "properties": { + "name": { + "type": "string", + "minLength": 1, + "default": "world" + }, + "useColor": { + "type": "boolean" + } + } +} +``` + +Suppose it would be preferred if the user was asked for their name. This can be accomplished by augmenting the `name` property definition with an `x-prompt` field. +```json +"x-prompt": "What is your name?" +``` +In most cases, only the text of the prompt is required. To minimize the amount of necessary configuration, the above _shorthand_ form is supported and will typically be all that is required. Full details regarding the _longhand_ form can be found in the **Configuration Reference** section. + +Adding a prompt to allow the user to decided whether the schematic will use color when executing its hello action is also very similar. The schema with both prompts would be as follows: +```json +{ + "properties": { + "name": { + "type": "string", + "minLength": 1, + "default": "world", + "x-prompt": "What is your name?" + }, + "useColor": { + "type": "boolean", + "x-prompt": "Would you like the response in color?" + } + } +} +``` + +Prompts have several different types which provide the ability to display an input method that best represents the schematic option's potential values. + +* `confirmation` - A **yes** or **no** question; ideal for boolean options +* `input` - textual input; ideal for string or number options +* `list` - a predefined set of items which may be selected + +When using the _shorthand_ form, the most appropriate type will automatically be selected based on the property's schema. In the example, the `name` prompt will use an `input` type because it it is a `string` property. The `useColor` prompt will use a `confirmation` type because it is a boolean property with `yes` corresponding to `true` and `no` corresponding to `false`. + +It is also important that the response from the user conforms to the contraints of the property. By specifying constraints using the JSON schema, the prompt runtime will automatically validate the response provided by the user. If the value is not acceptable, the user will be asked to enter a new value. This ensures that any values passed to the schematic will meet the expectations of the schematic's implementation and removes the need to add additional checks within the schematic's code. + +## Configuration Reference + +The `x-prompt` field supports two alternatives to enable a prompt for a schematic option. A shorthand form when additional customization is not required and a longhand form providing the ability for more control over the prompt. All user responses are validated against the property's schema. For example, string type properties can use a minimum length or regular expression constraint to control the allowed values. In the event the response fails validation, the user will be asked to enter a new value. + +### Longhand Form + +In the this form, the `x-prompt` field is an object with subfields that can be used to customize the behavior of the prompt. Note that some fields only apply to specific prompt types. + +| Field | Data Value | Default | +|-|-|-| +| `type` | `confirmation`, `input`, `list` | see shorthand section for details +| `message` | string | N/A (required) +| `items` | string and/or `label`/`value` object pair | only valid with type `list` + + +### Shorthand Form + +`x-prompt` [type: string] --> Question to display to the user. + +For this usage, the type of the prompt is determined by the type of the containing property. + +| Property Schema | Prompt Type | Notes | +|-|:-:|:-:| +| `"type": "boolean"` | `confirmation` | | +| `"type": "string"` | `input` | | +| `"type": "number"` | `input` | only valid numbers accepted | +| `"type": "integer"` | `input` | only valid numbers accepted | +| `"enum": [...]` | `list` | enum members become list selections + +### `x-prompt` Schema + +```json +{ + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "type": { "type": "string" }, + "message": { "type": "string" }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "label": { "type": "string" }, + "value": { } + }, + "required": [ "value" ] + } + ] + } + } + }, + "required": [ "message" ] + } + ] +} +``` \ No newline at end of file diff --git a/etc/README.md b/etc/README.md new file mode 100644 index 000000000000..b0c2447c9b1d --- /dev/null +++ b/etc/README.md @@ -0,0 +1,3 @@ +# `/etc` Folder + +This folder is for files that doesn't fit in other directories in the root folder. diff --git a/etc/api/BUILD b/etc/api/BUILD new file mode 100644 index 000000000000..3a10d4e9fb48 --- /dev/null +++ b/etc/api/BUILD @@ -0,0 +1,52 @@ +load("@npm_ts_api_guardian//:index.bzl", "ts_api_guardian_test") + +[ts_api_guardian_test( + name = "%s_%s_%sapi" % ( + bundle[0], + bundle[1], + bundle[2].replace("src/", "").replace("index", "").replace("_golden-api", "").replace("/", "_"), + ), + actual = "angular_cli/packages/%s/%s/npm_package/%s.d.ts" % ( + bundle[0], + bundle[1], + bundle[2], + ), + allow_module_identifiers = [ + "fs", + "ts", + "ajv", + "Symbol", + "webpack", + ], + data = glob([ + "%s/%s/**/*.d.ts" % ( + bundle[0], + bundle[1], + ), + ]) + [ + "//packages/%s/%s:npm_package" % ( + bundle[0], + bundle[1], + ), + ], + golden = "angular_cli/etc/api/%s/%s/%s.d.ts" % ( + bundle[0], + bundle[1], + bundle[2], + ), + # We don't want to analyse these exports nor add them to the golden files + # in most cases it's because Ts API Guardian doesn't support Symbol Aliases. + strip_export_pattern = [ + # @angular-devkit/architect + "^BuilderProgressState$", + # @angular-devkit/schematics + "^workflow$", + "^formats$", + # @angular-devkit/build-optimizer + "^buildOptimizerLoader$", + ], + # At the moment using this will ignore a big change + use_angular_tag_rules = False, +) for bundle in [b[:-len(".d.ts")].split("/", 2) for b in glob( + ["**/*.d.ts"], +)]] diff --git a/etc/api/angular_devkit/architect/src/index.d.ts b/etc/api/angular_devkit/architect/src/index.d.ts new file mode 100644 index 000000000000..1d7e5cb3e925 --- /dev/null +++ b/etc/api/angular_devkit/architect/src/index.d.ts @@ -0,0 +1,91 @@ +export declare class Architect { + constructor(_host: ArchitectHost, registry?: json.schema.SchemaRegistry, additionalJobRegistry?: experimental.jobs.Registry); + has(name: experimental.jobs.JobName): Observable; + scheduleBuilder(name: string, options: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; + scheduleTarget(target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; +} + +export interface BuilderContext { + readonly analytics: analytics.Analytics; + builder: BuilderInfo; + currentDirectory: string; + id: number; + logger: logging.LoggerApi; + target?: Target; + workspaceRoot: string; + addTeardown(teardown: () => (Promise | void)): void; + getBuilderNameForTarget(target: Target): Promise; + getTargetOptions(target: Target): Promise; + reportProgress(current: number, total?: number, status?: string): void; + reportRunning(): void; + reportStatus(status: string): void; + scheduleBuilder(builderName: string, options?: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; + scheduleTarget(target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions): Promise; + validateOptions(options: json.JsonObject, builderName: string): Promise; +} + +export interface BuilderHandlerFn { + (input: A, context: BuilderContext): BuilderOutputLike; +} + +export declare type BuilderInfo = json.JsonObject & { + builderName: string; + description: string; + optionSchema: json.schema.JsonSchema; +}; + +export declare type BuilderInput = json.JsonObject & RealBuilderInput; + +export declare type BuilderOutput = json.JsonObject & RealBuilderOutput; + +export declare type BuilderOutputLike = SubscribableOrPromise | BuilderOutput; + +export declare type BuilderProgress = json.JsonObject & RealBuilderProgress & TypedBuilderProgress; + +export declare type BuilderProgressReport = BuilderProgress & ({ + target?: Target; + builder: BuilderInfo; +}); + +export declare type BuilderRegistry = experimental.jobs.Registry; + +export interface BuilderRun { + id: number; + info: BuilderInfo; + output: Observable; + progress: Observable; + result: Promise; + stop(): Promise; +} + +export declare function createBuilder(fn: BuilderHandlerFn): Builder; + +export declare function isBuilderOutput(obj: any): obj is BuilderOutput; + +export interface ScheduleOptions { + analytics?: analytics.Analytics; + logger?: logging.Logger; +} + +export declare function scheduleTargetAndForget(context: BuilderContext, target: Target, overrides?: json.JsonObject, scheduleOptions?: ScheduleOptions): Observable; + +export declare type Target = json.JsonObject & RealTarget; + +export declare function targetFromTargetString(str: string): Target; + +export declare function targetStringFromTarget({ project, target, configuration }: Target): string; + +export declare type TypedBuilderProgress = ({ + state: BuilderProgressState.Stopped; +} | { + state: BuilderProgressState.Error; + error: json.JsonValue; +} | { + state: BuilderProgressState.Waiting; + status?: string; +} | { + state: BuilderProgressState.Running; + status?: string; + current: number; + total?: number; +}); diff --git a/etc/api/angular_devkit/architect/testing/index.d.ts b/etc/api/angular_devkit/architect/testing/index.d.ts new file mode 100644 index 000000000000..ccd2f03ff06d --- /dev/null +++ b/etc/api/angular_devkit/architect/testing/index.d.ts @@ -0,0 +1,37 @@ +export declare class TestingArchitectHost implements ArchitectHost { + currentDirectory: string; + workspaceRoot: string; + constructor(workspaceRoot?: string, currentDirectory?: string, _backendHost?: ArchitectHost | null); + addBuilder(builderName: string, builder: Builder, description?: string, optionSchema?: json.schema.JsonSchema): void; + addBuilderFromPackage(packageName: string): Promise; + addTarget(target: Target, builderName: string, options?: json.JsonObject): void; + getBuilderNameForTarget(target: Target): Promise; + getCurrentDirectory(): Promise; + getOptionsForTarget(target: Target): Promise; + getWorkspaceRoot(): Promise; + loadBuilder(info: BuilderInfo): Promise; + resolveBuilder(builderName: string): Promise; +} + +export declare class TestLogger extends logging.Logger { + constructor(name: string, parent?: logging.Logger | null); + clear(): void; + includes(message: string): boolean; + test(re: RegExp): boolean; +} + +export declare class TestProjectHost extends NodeJsSyncHost { + protected _templateRoot: Path; + constructor(_templateRoot: Path); + appendToFile(path: string, str: string): void; + copyFile(from: string, to: string): void; + fileMatchExists(dir: string, regex: RegExp): PathFragment | undefined; + initialize(): Observable; + replaceInFile(path: string, match: RegExp | string, replacement: string): void; + restore(): Observable; + root(): Path; + scopedSync(): virtualFs.SyncDelegateHost; + writeMultipleFiles(files: { + [path: string]: string | ArrayBufferLike | Buffer; + }): void; +} diff --git a/etc/api/angular_devkit/benchmark/src/index.d.ts b/etc/api/angular_devkit/benchmark/src/index.d.ts new file mode 100644 index 000000000000..9c3713d1a78b --- /dev/null +++ b/etc/api/angular_devkit/benchmark/src/index.d.ts @@ -0,0 +1,91 @@ +export interface AggregatedMetric extends Metric { + componentValues: number[]; +} + +export interface AggregatedProcessStats { + cpu: number; + ctime: number; + elapsed: number; + memory: number; + pid: number; + ppid: number; + processes: number; + timestamp: number; +} + +export declare const aggregateMetricGroups: (g1: MetricGroup, g2: MetricGroup) => MetricGroup; + +export declare const aggregateMetrics: (m1: Metric | AggregatedMetric, m2: Metric | AggregatedMetric) => AggregatedMetric; + +export declare type BenchmarkReporter = (command: Command, groups: MetricGroup[]) => void; + +export declare type Capture = (process: MonitoredProcess) => Observable; + +export declare class Command { + args: string[]; + cmd: string; + cwd: string; + expectedExitCode: number; + constructor(cmd: string, args?: string[], cwd?: string, expectedExitCode?: number); + toString(): string; +} + +export declare const cumulativeMovingAverage: (acc: number, val: number, accSize: number) => number; + +export declare const defaultReporter: (logger: logging.Logger) => BenchmarkReporter; + +export declare const defaultStatsCapture: Capture; + +export declare class LocalMonitoredProcess implements MonitoredProcess { + stats$: Observable; + stderr$: Observable; + stdout$: Observable; + constructor(command: Command); + run(): Observable; +} + +export declare function main({ args, stdout, stderr, }: MainOptions): Promise<0 | 1>; + +export interface MainOptions { + args: string[]; + stderr?: ProcessOutput; + stdout?: ProcessOutput; +} + +export declare const max: (v1: number, v2: number) => number; + +export declare class MaximumRetriesExceeded extends BaseException { + constructor(retries: number); +} + +export interface Metric { + componentValues?: number[]; + name: string; + unit: string; + value: number; +} + +export interface MetricGroup { + metrics: (Metric | AggregatedMetric)[]; + name: string; +} + +export interface MonitoredProcess { + stats$: Observable; + stderr$: Observable; + stdout$: Observable; + run(): Observable; + toString(): string; +} + +export declare function runBenchmark({ command, captures, reporters, iterations, retries, logger, }: RunBenchmarkOptions): Observable; + +export interface RunBenchmarkOptions { + captures: Capture[]; + command: Command; + expectedExitCode?: number; + iterations?: number; + logger?: logging.Logger; + reporters: BenchmarkReporter[]; + retries?: number; +} diff --git a/etc/api/angular_devkit/build_optimizer/src/_golden-api.d.ts b/etc/api/angular_devkit/build_optimizer/src/_golden-api.d.ts new file mode 100644 index 000000000000..83e1865b8cba --- /dev/null +++ b/etc/api/angular_devkit/build_optimizer/src/_golden-api.d.ts @@ -0,0 +1,25 @@ +export declare function buildOptimizer(options: BuildOptimizerOptions): TransformJavascriptOutput; + +export declare const buildOptimizerLoaderPath: string; + +export declare class BuildOptimizerWebpackPlugin { + apply(compiler: Compiler): void; +} + +export default function buildOptimizerLoader(this: webpack.loader.LoaderContext, content: string, previousSourceMap: RawSourceMap): void; + +export declare function getFoldFileTransformer(program: ts.Program): ts.TransformerFactory; + +export declare function getImportTslibTransformer(): ts.TransformerFactory; + +export declare function getPrefixClassesTransformer(): ts.TransformerFactory; + +export declare function getPrefixFunctionsTransformer(): ts.TransformerFactory; + +export declare function getScrubFileTransformer(program: ts.Program): ts.TransformerFactory; + +export declare function getScrubFileTransformerForCore(program: ts.Program): ts.TransformerFactory; + +export declare function getWrapEnumsTransformer(): ts.TransformerFactory; + +export declare function transformJavascript(options: TransformJavascriptOptions): TransformJavascriptOutput; diff --git a/etc/api/angular_devkit/core/node/_golden-api.d.ts b/etc/api/angular_devkit/core/node/_golden-api.d.ts new file mode 100644 index 000000000000..6577971a1802 --- /dev/null +++ b/etc/api/angular_devkit/core/node/_golden-api.d.ts @@ -0,0 +1,62 @@ +export declare function createConsoleLogger(verbose?: boolean, stdout?: ProcessOutput, stderr?: ProcessOutput, colors?: Partial string>>): logging.Logger; + +export declare function isDirectory(filePath: string): boolean; + +export declare function isFile(filePath: string): boolean; + +export declare class ModuleNotFoundException extends BaseException { + readonly basePath: string; + readonly code: string; + readonly moduleName: string; + constructor(moduleName: string, basePath: string); +} + +export declare class NodeJsAsyncHost implements virtualFs.Host { + readonly capabilities: virtualFs.HostCapabilities; + delete(path: Path): Observable; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + rename(from: Path, to: Path): Observable; + stat(path: Path): Observable> | null; + watch(path: Path, _options?: virtualFs.HostWatchOptions): Observable | null; + write(path: Path, content: virtualFs.FileBuffer): Observable; +} + +export declare class NodeJsSyncHost implements virtualFs.Host { + readonly capabilities: virtualFs.HostCapabilities; + delete(path: Path): Observable; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + rename(from: Path, to: Path): Observable; + stat(path: Path): Observable>; + watch(path: Path, _options?: virtualFs.HostWatchOptions): Observable | null; + write(path: Path, content: virtualFs.FileBuffer): Observable; +} + +export declare class NodeModuleJobRegistry implements core_experimental.jobs.Registry { + constructor(_resolveLocal?: boolean, _resolveGlobal?: boolean); + protected _resolve(name: string): string | null; + get(name: core_experimental.jobs.JobName): Observable | null>; +} + +export interface ProcessOutput { + write(buffer: string | Buffer): boolean; +} + +export declare function resolve(x: string, options: ResolveOptions): string; + +export interface ResolveOptions { + basedir: string; + checkGlobal?: boolean; + checkLocal?: boolean; + extensions?: string[]; + paths?: string[]; + preserveSymlinks?: boolean; + resolvePackageJson?: boolean; +} diff --git a/etc/api/angular_devkit/core/node/testing/index.d.ts b/etc/api/angular_devkit/core/node/testing/index.d.ts new file mode 100644 index 000000000000..0c9c161d0878 --- /dev/null +++ b/etc/api/angular_devkit/core/node/testing/index.d.ts @@ -0,0 +1,8 @@ +export declare class TempScopedNodeJsSyncHost extends virtualFs.ScopedHost { + protected _root: Path; + protected _sync: virtualFs.SyncDelegateHost; + readonly files: Path[]; + readonly root: Path; + readonly sync: virtualFs.SyncDelegateHost; + constructor(); +} diff --git a/etc/api/angular_devkit/core/src/_golden-api.d.ts b/etc/api/angular_devkit/core/src/_golden-api.d.ts new file mode 100644 index 000000000000..bd578e135b04 --- /dev/null +++ b/etc/api/angular_devkit/core/src/_golden-api.d.ts @@ -0,0 +1,1285 @@ +export interface AdditionalPropertiesValidatorError extends SchemaValidatorErrorBase { + keyword: 'additionalProperties'; + params: { + additionalProperty: string; + }; +} + +export declare function addUndefinedDefaults(value: JsonValue, _pointer: JsonPointer, schema?: JsonSchema): JsonValue; + +export declare class AliasHost extends ResolverHost { + protected _aliases: Map; + readonly aliases: Map; + protected _resolve(path: Path): Path; +} + +export declare class AmbiguousProjectPathException extends BaseException { + readonly path: Path; + readonly projects: ReadonlyArray; + constructor(path: Path, projects: ReadonlyArray); +} + +export interface Analytics { + event(category: string, action: string, options?: EventOptions): void; + flush(): Promise; + pageview(path: string, options?: PageviewOptions): void; + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void; + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void; +} + +export declare type AnalyticsForwarderFn = (report: JsonObject & AnalyticsReport) => void; + +export declare type AnalyticsReport = AnalyticsReportEvent | AnalyticsReportScreenview | AnalyticsReportPageview | AnalyticsReportTiming; + +export interface AnalyticsReportBase extends JsonObject { + kind: AnalyticsReportKind; +} + +export declare class AnalyticsReporter { + protected _analytics: Analytics; + constructor(_analytics: Analytics); + report(report: AnalyticsReport): void; +} + +export interface AnalyticsReportEvent extends AnalyticsReportBase { + action: string; + category: string; + kind: AnalyticsReportKind.Event; + options: JsonObject & EventOptions; +} + +export declare enum AnalyticsReportKind { + Event = "event", + Screenview = "screenview", + Pageview = "pageview", + Timing = "timing" +} + +export interface AnalyticsReportPageview extends AnalyticsReportBase { + kind: AnalyticsReportKind.Pageview; + options: JsonObject & PageviewOptions; + path: string; +} + +export interface AnalyticsReportScreenview extends AnalyticsReportBase { + appName: string; + kind: AnalyticsReportKind.Screenview; + options: JsonObject & ScreenviewOptions; + screenName: string; +} + +export interface AnalyticsReportTiming extends AnalyticsReportBase { + category: string; + kind: AnalyticsReportKind.Timing; + options: JsonObject & TimingOptions; + time: string | number; + variable: string; +} + +export declare function asPosixPath(path: Path): PosixPath; + +export declare function asWindowsPath(path: Path): WindowsPath; + +export declare class BaseException extends Error { + constructor(message?: string); +} + +export declare function basename(path: Path): PathFragment; + +export declare const bgBlack: (x: string) => string; + +export declare const bgBlue: (x: string) => string; + +export declare const bgCyan: (x: string) => string; + +export declare const bgGreen: (x: string) => string; + +export declare const bgMagenta: (x: string) => string; + +export declare const bgRed: (x: string) => string; + +export declare const bgWhite: (x: string) => string; + +export declare const bgYellow: (x: string) => string; + +export declare const black: (x: string) => string; + +export declare const blue: (x: string) => string; + +export declare const bold: (x: string) => string; + +export declare function buildJsonPointer(fragments: string[]): JsonPointer; + +export declare function camelize(str: string): string; + +export declare function capitalize(str: string): string; + +export declare class CircularDependencyFoundException extends BaseException { + constructor(); +} + +export declare function classify(str: string): string; + +export declare function clean(array: Array): Array; + +export declare namespace colors { + const reset: (x: string) => string; + const bold: (x: string) => string; + const dim: (x: string) => string; + const italic: (x: string) => string; + const underline: (x: string) => string; + const inverse: (x: string) => string; + const hidden: (x: string) => string; + const strikethrough: (x: string) => string; + const black: (x: string) => string; + const red: (x: string) => string; + const green: (x: string) => string; + const yellow: (x: string) => string; + const blue: (x: string) => string; + const magenta: (x: string) => string; + const cyan: (x: string) => string; + const white: (x: string) => string; + const grey: (x: string) => string; + const gray: (x: string) => string; + const bgBlack: (x: string) => string; + const bgRed: (x: string) => string; + const bgGreen: (x: string) => string; + const bgYellow: (x: string) => string; + const bgBlue: (x: string) => string; + const bgMagenta: (x: string) => string; + const bgCyan: (x: string) => string; + const bgWhite: (x: string) => string; +} + +export declare class ContentHasMutatedException extends BaseException { + constructor(path: string); +} + +export declare class CordHost extends SimpleMemoryHost { + protected _back: ReadonlyHost; + protected _filesToCreate: Set; + protected _filesToDelete: Set; + protected _filesToOverwrite: Set; + protected _filesToRename: Map; + protected _filesToRenameRevert: Map; + readonly backend: ReadonlyHost; + readonly capabilities: HostCapabilities; + constructor(_back: ReadonlyHost); + clone(): CordHost; + commit(host: Host, force?: boolean): Observable; + create(path: Path, content: FileBuffer): Observable; + delete(path: Path): Observable; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + overwrite(path: Path, content: FileBuffer): Observable; + read(path: Path): Observable; + records(): CordHostRecord[]; + rename(from: Path, to: Path): Observable; + stat(path: Path): Observable | null; + watch(path: Path, options?: HostWatchOptions): null; + willCreate(path: Path): boolean; + willDelete(path: Path): boolean; + willOverwrite(path: Path): boolean; + willRename(path: Path): boolean; + willRenameTo(path: Path, to: Path): boolean; + write(path: Path, content: FileBuffer): Observable; +} + +export interface CordHostCreate { + content: FileBuffer; + kind: 'create'; + path: Path; +} + +export interface CordHostDelete { + kind: 'delete'; + path: Path; +} + +export interface CordHostOverwrite { + content: FileBuffer; + kind: 'overwrite'; + path: Path; +} + +export declare type CordHostRecord = CordHostCreate | CordHostOverwrite | CordHostRename | CordHostDelete; + +export interface CordHostRename { + from: Path; + kind: 'rename'; + to: Path; +} + +export declare class CoreSchemaRegistry implements SchemaRegistry { + constructor(formats?: SchemaFormat[]); + protected _resolver(ref: string, validate: ajv.ValidateFunction): { + context?: ajv.ValidateFunction; + schema?: JsonObject; + }; + addFormat(format: SchemaFormat): void; + addPostTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addPreTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; + compile(schema: JsonSchema): Observable; + flatten(schema: JsonObject): Observable; + registerUriHandler(handler: UriHandler): void; + usePromptProvider(provider: PromptProvider): void; +} + +export declare function createWorkspaceHost(host: virtualFs.Host): WorkspaceHost; + +export interface CustomDimensionsAndMetricsOptions { + dimensions?: (boolean | number | string)[]; + metrics?: (boolean | number | string)[]; +} + +export declare const cyan: (x: string) => string; + +export declare function dasherize(str: string): string; + +export declare function decamelize(str: string): string; + +export declare function deepCopy(value: T): T; + +export declare type DefinitionCollectionListener = (name: string, action: 'add' | 'remove' | 'replace', newValue: V | undefined, oldValue: V | undefined) => void; + +export declare class DependencyNotFoundException extends BaseException { + constructor(); +} + +export declare const dim: (x: string) => string; + +export declare function dirname(path: Path): Path; + +export declare class Empty implements ReadonlyHost { + readonly capabilities: HostCapabilities; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + stat(path: Path): Observable | null>; +} + +export interface EventOptions extends CustomDimensionsAndMetricsOptions { + label?: string; + value?: string; +} + +export declare function extname(path: Path): string; + +export declare class FileAlreadyExistException extends BaseException { + constructor(path: string); +} + +export declare const fileBuffer: TemplateTag; + +export declare type FileBuffer = ArrayBuffer; + +export declare type FileBufferLike = ArrayBufferLike; + +export declare function fileBufferToString(fileBuffer: FileBuffer): string; + +export declare class FileDoesNotExistException extends BaseException { + constructor(path: string); +} + +export interface FormatValidatorError extends SchemaValidatorErrorBase { + keyword: 'format'; + params: { + format: string; + }; +} + +export declare class ForwardingAnalytics implements Analytics { + protected _fn: AnalyticsForwarderFn; + constructor(_fn: AnalyticsForwarderFn); + event(category: string, action: string, options?: EventOptions): void; + flush(): Promise; + pageview(path: string, options?: PageviewOptions): void; + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void; + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void; +} + +export declare function fragment(path: string): PathFragment; + +export declare function getSystemPath(path: Path): string; + +export declare function getTypesOfSchema(schema: JsonSchema): Set; + +export declare const gray: (x: string) => string; + +export declare const green: (x: string) => string; + +export declare const grey: (x: string) => string; + +export declare const hidden: (x: string) => string; + +export interface Host extends ReadonlyHost { + delete(path: Path): Observable; + rename(from: Path, to: Path): Observable; + watch(path: Path, options?: HostWatchOptions): Observable | null; + write(path: Path, content: FileBufferLike): Observable; +} + +export interface HostCapabilities { + synchronous: boolean; +} + +export interface HostWatchEvent { + readonly path: Path; + readonly time: Date; + readonly type: HostWatchEventType; +} + +export declare const enum HostWatchEventType { + Changed = 0, + Created = 1, + Deleted = 2, + Renamed = 3 +} + +export interface HostWatchOptions { + readonly persistent?: boolean; + readonly recursive?: boolean; +} + +export declare function indentBy(indentations: number): TemplateTag; + +export declare class IndentLogger extends Logger { + constructor(name: string, parent?: Logger | null, indentation?: string); +} + +export declare class InvalidJsonCharacterException extends JsonException { + character: number; + invalidChar: string; + line: number; + offset: number; + constructor(context: JsonParserContext); +} + +export declare class InvalidPathException extends BaseException { + constructor(path: string); +} + +export declare class InvalidUpdateRecordException extends BaseException { + constructor(); +} + +export declare const inverse: (x: string) => string; + +export declare function isAbsolute(p: Path): boolean; + +export declare function isJsonArray(value: JsonValue): value is JsonArray; + +export declare function isJsonObject(value: JsonValue): value is JsonObject; + +export declare function isObservable(obj: any | Observable): obj is Observable; + +export declare function isPromise(obj: any): obj is Promise; + +export declare const italic: (x: string) => string; + +export declare function join(p1: Path, ...others: string[]): Path; + +export declare function joinJsonPointer(root: JsonPointer, ...others: string[]): JsonPointer; + +export interface JsonArray extends Array { +} + +export interface JsonAstArray extends JsonAstNodeBase { + readonly elements: JsonAstNode[]; + readonly kind: 'array'; + readonly value: JsonArray; +} + +export interface JsonAstComment extends JsonAstNodeBase { + readonly content: string; + readonly kind: 'comment'; +} + +export interface JsonAstConstantFalse extends JsonAstNodeBase { + readonly kind: 'false'; + readonly value: false; +} + +export interface JsonAstConstantNull extends JsonAstNodeBase { + readonly kind: 'null'; + readonly value: null; +} + +export interface JsonAstConstantTrue extends JsonAstNodeBase { + readonly kind: 'true'; + readonly value: true; +} + +export interface JsonAstIdentifier extends JsonAstNodeBase { + readonly kind: 'identifier'; + readonly value: string; +} + +export interface JsonAstKeyValue extends JsonAstNodeBase { + readonly key: JsonAstString | JsonAstIdentifier; + readonly kind: 'keyvalue'; + readonly value: JsonAstNode; +} + +export interface JsonAstMultilineComment extends JsonAstNodeBase { + readonly content: string; + readonly kind: 'multicomment'; +} + +export declare type JsonAstNode = JsonAstNumber | JsonAstString | JsonAstIdentifier | JsonAstArray | JsonAstObject | JsonAstConstantFalse | JsonAstConstantNull | JsonAstConstantTrue; + +export interface JsonAstNodeBase { + readonly comments?: (JsonAstComment | JsonAstMultilineComment)[]; + readonly end: Position; + readonly start: Position; + readonly text: string; +} + +export interface JsonAstNumber extends JsonAstNodeBase { + readonly kind: 'number'; + readonly value: number; +} + +export interface JsonAstObject extends JsonAstNodeBase { + readonly kind: 'object'; + readonly properties: JsonAstKeyValue[]; + readonly value: JsonObject; +} + +export interface JsonAstString extends JsonAstNodeBase { + readonly kind: 'string'; + readonly value: string; +} + +export declare class JsonException extends BaseException { +} + +export interface JsonObject { + [prop: string]: JsonValue; +} + +export declare enum JsonParseMode { + Strict = 0, + CommentsAllowed = 1, + SingleQuotesAllowed = 2, + IdentifierKeyNamesAllowed = 4, + TrailingCommasAllowed = 8, + HexadecimalNumberAllowed = 16, + MultiLineStringAllowed = 32, + LaxNumberParsingAllowed = 64, + NumberConstantsAllowed = 128, + Default = 0, + Loose = 255, + Json = 0, + Json5 = 255 +} + +export interface JsonParserContext { + readonly mode: JsonParseMode; + readonly original: string; + position: Position; + previous: Position; +} + +export declare type JsonPointer = string & { + __PRIVATE_DEVKIT_JSON_POINTER: void; +}; + +export interface JsonSchemaVisitor { + (current: JsonObject | JsonArray, pointer: JsonPointer, parentSchema?: JsonObject | JsonArray, index?: string): void; +} + +export declare type JsonValue = JsonAstNode['value']; + +export interface JsonVisitor { + (value: JsonValue, pointer: JsonPointer, schema?: JsonObject, root?: JsonObject | JsonArray): Observable | JsonValue; +} + +export declare class LevelCapLogger extends LevelTransformLogger { + readonly levelCap: LogLevel; + readonly name: string; + readonly parent: Logger | null; + constructor(name: string, parent: Logger | null, levelCap: LogLevel); + static levelMap: { + [cap: string]: { + [level: string]: string; + }; + }; +} + +export declare class LevelTransformLogger extends Logger { + readonly levelTransform: (level: LogLevel) => LogLevel; + readonly name: string; + readonly parent: Logger | null; + constructor(name: string, parent: Logger | null, levelTransform: (level: LogLevel) => LogLevel); + createChild(name: string): Logger; + log(level: LogLevel, message: string, metadata?: JsonObject): void; +} + +export declare function levenshtein(a: string, b: string): number; + +export interface LimitValidatorError extends SchemaValidatorErrorBase { + keyword: 'maxItems' | 'minItems' | 'maxLength' | 'minLength' | 'maxProperties' | 'minProperties'; + params: { + limit: number; + }; +} + +export interface LogEntry extends LoggerMetadata { + level: LogLevel; + message: string; + timestamp: number; +} + +export declare class Logger extends Observable implements LoggerApi { + protected _metadata: LoggerMetadata; + protected _observable: Observable; + protected readonly _subject: Subject; + readonly name: string; + readonly parent: Logger | null; + constructor(name: string, parent?: Logger | null); + asApi(): LoggerApi; + complete(): void; + createChild(name: string): Logger; + debug(message: string, metadata?: JsonObject): void; + error(message: string, metadata?: JsonObject): void; + fatal(message: string, metadata?: JsonObject): void; + forEach(next: (value: LogEntry) => void, PromiseCtor?: typeof Promise): Promise; + info(message: string, metadata?: JsonObject): void; + lift(operator: Operator): Observable; + log(level: LogLevel, message: string, metadata?: JsonObject): void; + next(entry: LogEntry): void; + subscribe(): Subscription; + subscribe(observer: PartialObserver): Subscription; + subscribe(next?: (value: LogEntry) => void, error?: (error: Error) => void, complete?: () => void): Subscription; + toString(): string; + warn(message: string, metadata?: JsonObject): void; +} + +export interface LoggerApi { + createChild(name: string): Logger; + debug(message: string, metadata?: JsonObject): void; + error(message: string, metadata?: JsonObject): void; + fatal(message: string, metadata?: JsonObject): void; + info(message: string, metadata?: JsonObject): void; + log(level: LogLevel, message: string, metadata?: JsonObject): void; + warn(message: string, metadata?: JsonObject): void; +} + +export interface LoggerMetadata extends JsonObject { + name: string; + path: string[]; +} + +export declare class LoggingAnalytics implements Analytics { + protected _logger: Logger; + constructor(_logger: Logger); + event(category: string, action: string, options?: EventOptions): void; + flush(): Promise; + pageview(path: string, options?: PageviewOptions): void; + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void; + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void; +} + +export declare type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + +export declare const magenta: (x: string) => string; + +export declare function mapObject(obj: { + [k: string]: T; +}, mapper: (k: string, v: T) => V): { + [k: string]: V; +}; + +export declare class MergeConflictException extends BaseException { + constructor(path: string); +} + +export declare class MultiAnalytics implements Analytics { + protected _backends: Analytics[]; + constructor(_backends?: Analytics[]); + event(category: string, action: string, options?: EventOptions): void; + flush(): Promise; + pageview(path: string, options?: PageviewOptions): void; + push(...backend: Analytics[]): void; + screenview(screenName: string, appName: string, options?: ScreenviewOptions): void; + timing(category: string, variable: string, time: string | number, options?: TimingOptions): void; +} + +export declare enum NgCliAnalyticsDimensions { + CpuCount = 1, + CpuSpeed = 2, + RamInGigabytes = 3, + NodeVersion = 4, + NgAddCollection = 6, + NgBuildBuildEventLog = 7, + BuildErrors = 20 +} + +export declare const NgCliAnalyticsDimensionsFlagInfo: { + [name: string]: [string, string]; +}; + +export declare enum NgCliAnalyticsMetrics { + NgComponentCount = 1, + UNUSED_2 = 2, + UNUSED_3 = 3, + UNUSED_4 = 4, + BuildTime = 5, + NgOnInitCount = 6, + InitialChunkSize = 7, + TotalChunkCount = 8, + TotalChunkSize = 9, + LazyChunkCount = 10, + LazyChunkSize = 11, + AssetCount = 12, + AssetSize = 13, + PolyfillSize = 14, + CssSize = 15 +} + +export declare const NgCliAnalyticsMetricsFlagInfo: { + [name: string]: [string, string]; +}; + +export declare function noCacheNormalize(path: string): Path; + +export declare class NoopAnalytics implements Analytics { + event(): void; + flush(): Promise; + pageview(): void; + screenview(): void; + timing(): void; +} + +export declare function normalize(path: string): Path; + +export declare const NormalizedRoot: Path; + +export declare const NormalizedSep: Path; + +export declare class NullLogger extends Logger { + constructor(parent?: Logger | null); + asApi(): LoggerApi; +} + +export declare function oneLine(strings: TemplateStringsArray, ...values: any[]): string; + +export interface PageviewOptions extends CustomDimensionsAndMetricsOptions { + hostname?: string; + title?: string; +} + +export declare function parseJson(input: string, mode?: JsonParseMode, options?: ParseJsonOptions): JsonValue; + +export declare function parseJsonAst(input: string, mode?: JsonParseMode): JsonAstNode; + +export interface ParseJsonOptions { + path?: string; +} + +export declare function parseJsonPointer(pointer: JsonPointer): string[]; + +export declare class PartiallyOrderedSet implements Set { + readonly [Symbol.toStringTag]: 'Set'; + readonly size: number; + [Symbol.iterator](): IterableIterator; + protected _checkCircularDependencies(item: T, deps: Set): void; + add(item: T, deps?: (Set | T[])): this; + clear(): void; + delete(item: T): boolean; + entries(): IterableIterator<[T, T]>; + forEach(callbackfn: (value: T, value2: T, set: PartiallyOrderedSet) => void, thisArg?: any): void; + has(item: T): boolean; + keys(): IterableIterator; + values(): IterableIterator; +} + +export declare const path: TemplateTag; + +export declare type Path = string & { + __PRIVATE_DEVKIT_PATH: void; +}; + +export declare class PathCannotBeFragmentException extends BaseException { + constructor(path: string); +} + +export declare type PathFragment = Path & { + __PRIVATE_DEVKIT_PATH_FRAGMENT: void; +}; + +export declare class PathIsDirectoryException extends BaseException { + constructor(path: string); +} + +export declare class PathIsFileException extends BaseException { + constructor(path: string); +} + +export declare class PathMustBeAbsoluteException extends BaseException { + constructor(path: string); +} + +export declare class PathSpecificJsonException extends JsonException { + exception: JsonException; + path: string; + constructor(path: string, exception: JsonException); +} + +export declare class PatternMatchingHost extends ResolverHost { + protected _patterns: Map; + protected _resolve(path: Path): Path; + addPattern(pattern: string | string[], replacementFn: ReplacementFunction): void; +} + +export interface Position { + readonly character: number; + readonly line: number; + readonly offset: number; +} + +export declare type PosixPath = string & { + __PRIVATE_DEVKIT_POSIX_PATH: void; +}; + +export declare class PriorityQueue { + readonly size: number; + constructor(_comparator: (x: T, y: T) => number); + clear(): void; + peek(): T | undefined; + pop(): T | undefined; + push(item: T): void; + toArray(): Array; +} + +export interface ProjectDefinition { + readonly extensions: Record; + prefix?: string; + root: string; + sourceRoot?: string; + readonly targets: TargetDefinitionCollection; +} + +export declare class ProjectDefinitionCollection extends DefinitionCollection { + constructor(initial?: Record, listener?: DefinitionCollectionListener); + add(definition: { + name: string; + root: string; + sourceRoot?: string; + prefix?: string; + targets?: Record; + [key: string]: unknown; + }): ProjectDefinition; + set(name: string, value: ProjectDefinition): this; +} + +export declare class ProjectNotFoundException extends BaseException { + constructor(name: string); +} + +export declare class ProjectToolNotFoundException extends BaseException { + constructor(name: string); +} + +export interface PromptDefinition { + default?: string | string[] | number | boolean | null; + id: string; + items?: Array; + message: string; + multiselect?: boolean; + raw?: string | JsonObject; + type: string; + validator?: (value: string) => boolean | string | Promise; +} + +export declare type PromptProvider = (definitions: Array) => SubscribableOrPromise<{ + [id: string]: JsonValue; +}>; + +export interface ReadonlyHost { + readonly capabilities: HostCapabilities; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + stat(path: Path): Observable | null> | null; +} + +export declare function readWorkspace(path: string, host: WorkspaceHost, format?: WorkspaceFormat): Promise<{ + workspace: WorkspaceDefinition; +}>; + +export declare const red: (x: string) => string; + +export interface ReferenceResolver { + (ref: string, context?: ContextT): { + context?: ContextT; + schema?: JsonObject; + }; +} + +export interface RefValidatorError extends SchemaValidatorErrorBase { + keyword: '$ref'; + params: { + ref: string; + }; +} + +export declare function relative(from: Path, to: Path): Path; + +export declare type ReplacementFunction = (path: Path) => Path; + +export interface RequiredValidatorError extends SchemaValidatorErrorBase { + keyword: 'required'; + params: { + missingProperty: string; + }; +} + +export declare const reset: (x: string) => string; + +export declare function resetNormalizeCache(): void; + +export declare function resolve(p1: Path, p2: Path): Path; + +export declare abstract class ResolverHost implements Host { + protected _delegate: Host; + readonly capabilities: HostCapabilities; + constructor(_delegate: Host); + protected abstract _resolve(path: Path): Path; + delete(path: Path): Observable; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + rename(from: Path, to: Path): Observable; + stat(path: Path): Observable | null> | null; + watch(path: Path, options?: HostWatchOptions): Observable | null; + write(path: Path, content: FileBuffer): Observable; +} + +export declare class SafeReadonlyHost implements ReadonlyHost { + readonly capabilities: HostCapabilities; + constructor(_delegate: ReadonlyHost); + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + stat(path: Path): Observable | null> | null; +} + +export interface SchemaFormat { + formatter: SchemaFormatter; + name: string; +} + +export interface SchemaFormatter { + readonly async: boolean; + validate(data: any): boolean | Observable; +} + +export interface SchemaKeywordValidator { + (data: JsonValue, schema: JsonValue, parent: JsonObject | JsonArray | undefined, parentProperty: string | number | undefined, pointer: JsonPointer, rootData: JsonValue): boolean | Observable; +} + +export interface SchemaRegistry { + addFormat(format: SchemaFormat): void; + addPostTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addPreTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; + compile(schema: Object): Observable; + flatten(schema: JsonObject | string): Observable; + usePromptProvider(provider: PromptProvider): void; +} + +export declare class SchemaValidationException extends BaseException { + readonly errors: SchemaValidatorError[]; + constructor(errors?: SchemaValidatorError[], baseMessage?: string); + static createMessages(errors?: SchemaValidatorError[]): string[]; +} + +export interface SchemaValidator { + (data: JsonValue, options?: SchemaValidatorOptions): Observable; +} + +export declare type SchemaValidatorError = RefValidatorError | LimitValidatorError | AdditionalPropertiesValidatorError | FormatValidatorError | RequiredValidatorError; + +export interface SchemaValidatorErrorBase { + data?: JsonValue; + dataPath: string; + keyword: string; + message?: string; +} + +export interface SchemaValidatorOptions { + applyPostTransforms?: boolean; + applyPreTransforms?: boolean; + withPrompts?: boolean; +} + +export interface SchemaValidatorResult { + data: JsonValue; + errors?: SchemaValidatorError[]; + success: boolean; +} + +export declare class ScopedHost extends ResolverHost { + protected _root: Path; + constructor(delegate: Host, _root?: Path); + protected _resolve(path: Path): Path; +} + +export interface ScreenviewOptions extends CustomDimensionsAndMetricsOptions { + appId?: string; + appInstallerId?: string; + appVersion?: string; +} + +export declare class SimpleMemoryHost implements Host<{}> { + protected _cache: Map>; + readonly capabilities: HostCapabilities; + constructor(); + protected _delete(path: Path): void; + protected _exists(path: Path): boolean; + protected _isDirectory(path: Path): boolean; + protected _isFile(path: Path): boolean; + protected _list(path: Path): PathFragment[]; + protected _newDirStats(): { + inspect(): string; + isFile(): boolean; + isDirectory(): boolean; + size: number; + atime: Date; + ctime: Date; + mtime: Date; + birthtime: Date; + content: null; + }; + protected _newFileStats(content: FileBuffer, oldStats?: Stats): { + inspect(): string; + isFile(): boolean; + isDirectory(): boolean; + size: number; + atime: Date; + ctime: Date; + mtime: Date; + birthtime: Date; + content: ArrayBuffer; + }; + protected _read(path: Path): FileBuffer; + protected _rename(from: Path, to: Path): void; + protected _stat(path: Path): Stats | null; + protected _toAbsolute(path: Path): Path; + protected _updateWatchers(path: Path, type: HostWatchEventType): void; + protected _watch(path: Path, options?: HostWatchOptions): Observable; + protected _write(path: Path, content: FileBuffer): void; + delete(path: Path): Observable; + exists(path: Path): Observable; + isDirectory(path: Path): Observable; + isFile(path: Path): Observable; + list(path: Path): Observable; + read(path: Path): Observable; + rename(from: Path, to: Path): Observable; + stat(path: Path): Observable | null> | null; + watch(path: Path, options?: HostWatchOptions): Observable | null; + write(path: Path, content: FileBuffer): Observable; +} + +export interface SimpleMemoryHostStats { + readonly content: FileBuffer | null; +} + +export interface SmartDefaultProvider { + (schema: JsonObject): T | Observable; +} + +export declare function split(path: Path): PathFragment[]; + +export declare type Stats = T & { + isFile(): boolean; + isDirectory(): boolean; + readonly size: number; + readonly atime: Date; + readonly mtime: Date; + readonly ctime: Date; + readonly birthtime: Date; +}; + +export declare const strikethrough: (x: string) => string; + +export declare function stringToFileBuffer(str: string): FileBuffer; + +export declare function stripIndent(strings: TemplateStringsArray, ...values: any[]): string; + +export declare function stripIndents(strings: TemplateStringsArray, ...values: any[]): string; + +export declare class SyncDelegateHost { + protected _delegate: Host; + readonly capabilities: HostCapabilities; + readonly delegate: Host; + constructor(_delegate: Host); + protected _doSyncCall(observable: Observable): ResultT; + delete(path: Path): void; + exists(path: Path): boolean; + isDirectory(path: Path): boolean; + isFile(path: Path): boolean; + list(path: Path): PathFragment[]; + read(path: Path): FileBuffer; + rename(from: Path, to: Path): void; + stat(path: Path): Stats | null; + watch(path: Path, options?: HostWatchOptions): Observable | null; + write(path: Path, content: FileBufferLike): void; +} + +export declare class SynchronousDelegateExpectedException extends BaseException { + constructor(); +} + +export interface TargetDefinition { + builder: string; + configurations?: Record | undefined>; + options?: Record; +} + +export declare class TargetDefinitionCollection extends DefinitionCollection { + constructor(initial?: Record, listener?: DefinitionCollectionListener); + add(definition: { + name: string; + } & TargetDefinition): TargetDefinition; + set(name: string, value: TargetDefinition): this; +} + +export declare function template(content: string, options?: TemplateOptions): (input: T) => string; + +export interface TemplateAst { + children: TemplateAstNode[]; + content: string; + fileName: string; +} + +export interface TemplateAstBase { + end: Position; + start: Position; +} + +export interface TemplateAstComment extends TemplateAstBase { + kind: 'comment'; + text: string; +} + +export interface TemplateAstContent extends TemplateAstBase { + content: string; + kind: 'content'; +} + +export interface TemplateAstEscape extends TemplateAstBase { + expression: string; + kind: 'escape'; +} + +export interface TemplateAstEvaluate extends TemplateAstBase { + expression: string; + kind: 'evaluate'; +} + +export interface TemplateAstInterpolate extends TemplateAstBase { + expression: string; + kind: 'interpolate'; +} + +export declare type TemplateAstNode = TemplateAstContent | TemplateAstEvaluate | TemplateAstComment | TemplateAstEscape | TemplateAstInterpolate; + +export interface TemplateOptions { + fileName?: string; + module?: boolean | { + exports: {}; + }; + sourceMap?: boolean; + sourceRoot?: string; + sourceURL?: string; +} + +export declare function templateParser(sourceText: string, fileName: string): TemplateAst; + +export interface TemplateTag { + (template: TemplateStringsArray, ...substitutions: any[]): R; +} + +export declare namespace test { + type TestLogRecord = { + kind: 'write' | 'read' | 'delete' | 'list' | 'exists' | 'isDirectory' | 'isFile' | 'stat' | 'watch'; + path: Path; + } | { + kind: 'rename'; + from: Path; + to: Path; + }; + class TestHost extends SimpleMemoryHost { + protected _records: TestLogRecord[]; + protected _sync: SyncDelegateHost<{}>; + readonly files: Path[]; + readonly records: TestLogRecord[]; + readonly sync: SyncDelegateHost<{}>; + constructor(map?: { + [path: string]: string; + }); + $exists(path: string): boolean; + $isDirectory(path: string): boolean; + $isFile(path: string): boolean; + $list(path: string): PathFragment[]; + $read(path: string): string; + $write(path: string, content: string): void; + protected _delete(path: Path): void; + protected _exists(path: Path): boolean; + protected _isDirectory(path: Path): boolean; + protected _isFile(path: Path): boolean; + protected _list(path: Path): PathFragment[]; + protected _read(path: Path): ArrayBuffer; + protected _rename(from: Path, to: Path): void; + protected _stat(path: Path): Stats | null; + protected _watch(path: Path, options?: HostWatchOptions): Observable; + protected _write(path: Path, content: FileBuffer): void; + clearRecords(): void; + clone(): TestHost; + } +} + +export interface TimingOptions extends CustomDimensionsAndMetricsOptions { + label?: string; +} + +export declare class TransformLogger extends Logger { + constructor(name: string, transform: (stream: Observable) => Observable, parent?: Logger | null); +} + +export declare function trimNewlines(strings: TemplateStringsArray, ...values: any[]): string; + +export declare const underline: (x: string) => string; + +export declare function underscore(str: string): string; + +export declare class UnexpectedEndOfInputException extends JsonException { + constructor(_context: JsonParserContext); +} + +export declare class UnimplementedException extends BaseException { + constructor(); +} + +export declare class UnknownException extends BaseException { + constructor(message: string); +} + +export declare class UnsupportedPlatformException extends BaseException { + constructor(); +} + +export declare type UriHandler = (uri: string) => Observable | Promise | null | undefined; + +export declare function visitJson(json: JsonValue, visitor: JsonVisitor, schema?: JsonSchema, refResolver?: ReferenceResolver, context?: ContextT): Observable; + +export declare function visitJsonSchema(schema: JsonSchema, visitor: JsonSchemaVisitor): void; + +export declare const white: (x: string) => string; + +export declare type WindowsPath = string & { + __PRIVATE_DEVKIT_WINDOWS_PATH: void; +}; + +export declare class Workspace { + readonly host: virtualFs.Host<{}>; + readonly newProjectRoot: string | undefined; + readonly root: Path; + readonly version: number; + constructor(_root: Path, _host: virtualFs.Host<{}>, registry?: schema.CoreSchemaRegistry); + getCli(): WorkspaceTool; + getDefaultProjectName(): string | null; + getProject(projectName: string): WorkspaceProject; + getProjectByPath(path: Path): string | null; + getProjectCli(projectName: string): WorkspaceTool; + getProjectSchematics(projectName: string): WorkspaceTool; + getProjectTargets(projectName: string): WorkspaceTool; + getSchematics(): WorkspaceTool; + getTargets(): WorkspaceTool; + listProjectNames(): string[]; + loadWorkspaceFromHost(workspacePath: Path): Observable; + loadWorkspaceFromJson(json: {}): Observable; + validateAgainstSchema(contentJson: {}, schemaJson: JsonObject): Observable; + protected static _workspaceFileNames: string[]; + static findWorkspaceFile(host: virtualFs.Host<{}>, path: Path): Promise; + static fromPath(host: virtualFs.Host<{}>, path: Path, registry: schema.CoreSchemaRegistry): Promise; +} + +export interface WorkspaceDefinition { + readonly extensions: Record; + readonly projects: ProjectDefinitionCollection; +} + +export declare class WorkspaceFileNotFoundException extends BaseException { + constructor(path: Path); +} + +export declare enum WorkspaceFormat { + JSON = 0 +} + +export interface WorkspaceHost { + isDirectory(path: string): Promise; + isFile(path: string): Promise; + readFile(path: string): Promise; + writeFile(path: string, data: string): Promise; +} + +export declare class WorkspaceNotYetLoadedException extends BaseException { + constructor(); +} + +export interface WorkspaceProject { + architect?: WorkspaceTool; + cli?: WorkspaceTool; + prefix: string; + projectType: "application" | "library"; + root: string; + schematics?: WorkspaceTool; + sourceRoot?: string; + targets?: WorkspaceTool; +} + +export interface WorkspaceSchema { + $schema?: string; + architect?: WorkspaceTool; + cli?: WorkspaceTool; + defaultProject?: string; + newProjectRoot?: string; + projects: { + [k: string]: WorkspaceProject; + }; + schematics?: WorkspaceTool; + targets?: WorkspaceTool; + version: number; +} + +export interface WorkspaceTool { + $schema?: string; + [k: string]: any; +} + +export declare class WorkspaceToolNotFoundException extends BaseException { + constructor(name: string); +} + +export declare function writeWorkspace(workspace: WorkspaceDefinition, host: WorkspaceHost, path?: string, format?: WorkspaceFormat): Promise; + +export declare const yellow: (x: string) => string; diff --git a/etc/api/angular_devkit/schematics/src/_golden-api.d.ts b/etc/api/angular_devkit/schematics/src/_golden-api.d.ts new file mode 100644 index 000000000000..2c86d1d05ca4 --- /dev/null +++ b/etc/api/angular_devkit/schematics/src/_golden-api.d.ts @@ -0,0 +1,627 @@ +export declare type Action = CreateFileAction | OverwriteFileAction | RenameFileAction | DeleteFileAction; + +export interface ActionBase { + readonly id: number; + readonly parent: number; + readonly path: Path; +} + +export declare class ActionList implements Iterable { + readonly length: number; + [Symbol.iterator](): IterableIterator; + protected _action(action: Partial): void; + create(path: Path, content: Buffer): void; + delete(path: Path): void; + find(predicate: (value: Action) => boolean): Action | null; + forEach(fn: (value: Action, index: number, array: Action[]) => void, thisArg?: {}): void; + get(i: number): Action; + has(action: Action): boolean; + optimize(): void; + overwrite(path: Path, content: Buffer): void; + push(action: Action): void; + rename(path: Path, to: Path): void; +} + +export declare function apply(source: Source, rules: Rule[]): Source; + +export declare function applyContentTemplate(options: T): FileOperator; + +export declare function applyPathTemplate(data: T, options?: PathTemplateOptions): FileOperator; + +export declare function applyTemplates(options: T): Rule; + +export declare function applyToSubtree(path: string, rules: Rule[]): Rule; + +export declare function asSource(rule: Rule): Source; + +export declare type AsyncFileOperator = (tree: FileEntry) => Observable; + +export declare abstract class BaseWorkflow implements Workflow { + protected _context: WorkflowExecutionContext[]; + protected _dryRun: boolean; + protected _engine: Engine<{}, {}>; + protected _engineHost: EngineHost<{}, {}>; + protected _force: boolean; + protected _host: virtualFs.Host; + protected _lifeCycle: Subject; + protected _registry: schema.CoreSchemaRegistry; + protected _reporter: Subject; + readonly context: Readonly; + readonly engine: Engine<{}, {}>; + readonly engineHost: EngineHost<{}, {}>; + readonly lifeCycle: Observable; + readonly registry: schema.SchemaRegistry; + readonly reporter: Observable; + constructor(options: BaseWorkflowOptions); + protected _createSinks(): Sink[]; + execute(options: Partial & RequiredWorkflowExecutionContext): Observable; +} + +export interface BaseWorkflowOptions { + dryRun?: boolean; + engineHost: EngineHost<{}, {}>; + force?: boolean; + host: virtualFs.Host; + registry?: schema.CoreSchemaRegistry; +} + +export declare function branchAndMerge(rule: Rule, strategy?: MergeStrategy): Rule; + +export declare function callRule(rule: Rule, input: Tree | Observable, context: SchematicContext): Observable; + +export declare function callSource(source: Source, context: SchematicContext): Observable; + +export declare function chain(rules: Rule[]): Rule; + +export declare class CircularCollectionException extends BaseException { + constructor(name: string); +} + +export interface Collection { + readonly baseDescriptions?: Array>; + readonly description: CollectionDescription; + createSchematic(name: string, allowPrivate?: boolean): Schematic; + listSchematicNames(): string[]; +} + +export declare type CollectionDescription = CollectionMetadataT & { + readonly name: string; + readonly extends?: string[]; +}; + +export declare class CollectionImpl implements Collection { + readonly baseDescriptions?: CollectionDescription[] | undefined; + readonly description: CollectionDescription; + readonly name: string; + constructor(_description: CollectionDescription, _engine: SchematicEngine, baseDescriptions?: CollectionDescription[] | undefined); + createSchematic(name: string, allowPrivate?: boolean): Schematic; + listSchematicNames(): string[]; +} + +export declare function composeFileOperators(operators: FileOperator[]): FileOperator; + +export declare class ContentHasMutatedException extends BaseException { + constructor(path: string); +} + +export declare function contentTemplate(options: T): Rule; + +export interface CreateFileAction extends ActionBase { + readonly content: Buffer; + readonly kind: 'c'; +} + +export declare class DelegateTree implements Tree { + protected _other: Tree; + readonly actions: Action[]; + readonly root: DirEntry; + constructor(_other: Tree); + apply(action: Action, strategy?: MergeStrategy): void; + beginUpdate(path: string): UpdateRecorder; + branch(): Tree; + commitUpdate(record: UpdateRecorder): void; + create(path: string, content: Buffer | string): void; + delete(path: string): void; + exists(path: string): boolean; + get(path: string): FileEntry | null; + getDir(path: string): DirEntry; + merge(other: Tree, strategy?: MergeStrategy): void; + overwrite(path: string, content: Buffer | string): void; + read(path: string): Buffer | null; + rename(from: string, to: string): void; + visit(visitor: FileVisitor): void; +} + +export interface DeleteFileAction extends ActionBase { + readonly kind: 'd'; +} + +export interface DirEntry { + readonly parent: DirEntry | null; + readonly path: Path; + readonly subdirs: PathFragment[]; + readonly subfiles: PathFragment[]; + dir(name: PathFragment): DirEntry; + file(name: PathFragment): FileEntry | null; + visit(visitor: FileVisitor): void; +} + +export interface DryRunCreateEvent { + content: Buffer; + kind: 'create'; + path: string; +} + +export interface DryRunDeleteEvent { + kind: 'delete'; + path: string; +} + +export interface DryRunErrorEvent { + description: 'alreadyExist' | 'doesNotExist'; + kind: 'error'; + path: string; +} + +export declare type DryRunEvent = DryRunErrorEvent | DryRunDeleteEvent | DryRunCreateEvent | DryRunUpdateEvent | DryRunRenameEvent; + +export interface DryRunRenameEvent { + kind: 'rename'; + path: string; + to: string; +} + +export declare class DryRunSink extends HostSink { + protected _fileAlreadyExistExceptionSet: Set; + protected _fileDoesNotExistExceptionSet: Set; + protected _subject: Subject; + readonly reporter: Observable; + constructor(dir: string, force?: boolean); + constructor(host: virtualFs.Host, force?: boolean); + _done(): Observable; + protected _fileAlreadyExistException(path: string): void; + protected _fileDoesNotExistException(path: string): void; +} + +export interface DryRunUpdateEvent { + content: Buffer; + kind: 'update'; + path: string; +} + +export declare function empty(): Source; + +export declare class EmptyTree extends HostTree { + constructor(); +} + +export interface Engine { + readonly defaultMergeStrategy: MergeStrategy; + readonly workflow: Workflow | null; + createCollection(name: string): Collection; + createContext(schematic: Schematic, parent?: Partial>, executionOptions?: Partial): TypedSchematicContext; + createSchematic(name: string, collection: Collection): Schematic; + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source; + executePostTasks(): Observable; + transformOptions(schematic: Schematic, options: OptionT, context?: TypedSchematicContext): Observable; +} + +export interface EngineHost { + readonly defaultMergeStrategy?: MergeStrategy; + createCollectionDescription(name: string): CollectionDescription; + createSchematicDescription(name: string, collection: CollectionDescription): SchematicDescription | null; + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source | null; + createTaskExecutor(name: string): Observable; + getSchematicRuleFactory(schematic: SchematicDescription, collection: CollectionDescription): RuleFactory; + hasTaskExecutor(name: string): boolean; + listSchematicNames(collection: CollectionDescription): string[]; + listSchematics(collection: Collection): string[]; + transformContext(context: TypedSchematicContext): TypedSchematicContext | void; + transformOptions(schematic: SchematicDescription, options: OptionT, context?: TypedSchematicContext): Observable; +} + +export interface ExecutionOptions { + interactive: boolean; + scope: string; +} + +export declare function externalSchematic(collectionName: string, schematicName: string, options: OptionT, executionOptions?: Partial): Rule; + +export declare class FileAlreadyExistException extends BaseException { + constructor(path: string); +} + +export declare class FileDoesNotExistException extends BaseException { + constructor(path: string); +} + +export interface FileEntry { + readonly content: Buffer; + readonly path: Path; +} + +export declare type FileOperator = (entry: FileEntry) => FileEntry | null; + +export interface FilePredicate { + (path: Path, entry?: Readonly | null): T; +} + +export declare class FileSystemSink extends HostSink { + constructor(dir: string, force?: boolean); +} + +export declare type FileVisitor = FilePredicate; + +export declare const FileVisitorCancelToken: symbol; + +export declare function filter(predicate: FilePredicate): Rule; + +export declare class FilterHostTree extends HostTree { + constructor(tree: HostTree, filter?: FilePredicate); +} + +export declare function forEach(operator: FileOperator): Rule; + +export declare class HostCreateTree extends HostTree { + constructor(host: virtualFs.ReadonlyHost); +} + +export declare class HostDirEntry implements DirEntry { + protected _host: virtualFs.SyncDelegateHost; + protected _tree: Tree; + readonly parent: DirEntry | null; + readonly path: Path; + readonly subdirs: PathFragment[]; + readonly subfiles: PathFragment[]; + constructor(parent: DirEntry | null, path: Path, _host: virtualFs.SyncDelegateHost, _tree: Tree); + dir(name: PathFragment): DirEntry; + file(name: PathFragment): FileEntry | null; + visit(visitor: FileVisitor): void; +} + +export declare class HostSink extends SimpleSinkBase { + protected _filesToCreate: Map; + protected _filesToDelete: Set; + protected _filesToRename: Set<[Path, Path]>; + protected _filesToUpdate: Map; + protected _force: boolean; + protected _host: virtualFs.Host; + constructor(_host: virtualFs.Host, _force?: boolean); + protected _createFile(path: Path, content: Buffer): Observable; + protected _deleteFile(path: Path): Observable; + _done(): Observable; + protected _overwriteFile(path: Path, content: Buffer): Observable; + protected _renameFile(from: Path, to: Path): Observable; + protected _validateCreateAction(action: CreateFileAction): Observable; + protected _validateFileExists(p: Path): Observable; +} + +export declare class HostTree implements Tree { + protected _backend: virtualFs.ReadonlyHost<{}>; + readonly actions: Action[]; + readonly root: DirEntry; + constructor(_backend?: virtualFs.ReadonlyHost<{}>); + protected _normalizePath(path: string): Path; + protected _willCreate(path: Path): boolean; + protected _willDelete(path: Path): boolean; + protected _willOverwrite(path: Path): boolean; + protected _willRename(path: Path): boolean; + apply(action: Action, strategy?: MergeStrategy): void; + beginUpdate(path: string): UpdateRecorder; + branch(): Tree; + commitUpdate(record: UpdateRecorder): void; + create(path: string, content: Buffer | string): void; + delete(path: string): void; + exists(path: string): boolean; + get(path: string): FileEntry | null; + getDir(path: string): DirEntry; + merge(other: Tree, strategy?: MergeStrategy): void; + optimize(): this; + overwrite(path: string, content: Buffer | string): void; + read(path: string): Buffer | null; + rename(from: string, to: string): void; + visit(visitor: FileVisitor): void; + static isHostTree(tree: Tree): tree is HostTree; +} + +export declare const htmlSelectorFormat: schema.SchemaFormat; + +export declare class InvalidPipeException extends BaseException { + constructor(name: string); +} + +export declare class InvalidRuleResultException extends BaseException { + constructor(value?: {}); +} + +export declare class InvalidSchematicsNameException extends BaseException { + constructor(name: string); +} + +export declare class InvalidSourceResultException extends BaseException { + constructor(value?: {}); +} + +export declare class InvalidUpdateRecordException extends BaseException { + constructor(); +} + +export declare function isAction(action: any): action is Action; + +export declare function isContentAction(action: Action): action is CreateFileAction | OverwriteFileAction; + +export interface LifeCycleEvent { + kind: 'start' | 'end' | 'workflow-start' | 'workflow-end' | 'post-tasks-start' | 'post-tasks-end'; +} + +export declare class MergeConflictException extends BaseException { + constructor(path: string); +} + +export declare enum MergeStrategy { + AllowOverwriteConflict = 2, + AllowCreationConflict = 4, + AllowDeleteConflict = 8, + Default = 0, + Error = 1, + ContentOnly = 2, + Overwrite = 14 +} + +export declare function mergeWith(source: Source, strategy?: MergeStrategy): Rule; + +export declare function move(from: string, to?: string): Rule; + +export declare function noop(): Rule; + +export declare class OptionIsNotDefinedException extends BaseException { + constructor(name: string); +} + +export interface OverwriteFileAction extends ActionBase { + readonly content: Buffer; + readonly kind: 'o'; +} + +export declare function partitionApplyMerge(predicate: FilePredicate, ruleYes: Rule, ruleNo?: Rule): Rule; + +export declare const pathFormat: schema.SchemaFormat; + +export declare function pathTemplate(options: T): Rule; + +export declare type PathTemplateData = { + [key: string]: PathTemplateValue | PathTemplateData | PathTemplatePipeFunction; +}; + +export interface PathTemplateOptions { + interpolationEnd: string; + interpolationStart: string; + pipeSeparator?: string; +} + +export declare type PathTemplatePipeFunction = (x: string) => PathTemplateValue; + +export declare type PathTemplateValue = boolean | string | number | undefined; + +export declare class PrivateSchematicException extends BaseException { + constructor(name: string, collection: CollectionDescription<{}>); +} + +export interface RandomOptions { + multi?: boolean | number; + multiFiles?: boolean | number; + root?: string; +} + +export interface RenameFileAction extends ActionBase { + readonly kind: 'r'; + readonly to: Path; +} + +export declare function renameTemplateFiles(): Rule; + +export interface RequiredWorkflowExecutionContext { + collection: string; + options: object; + schematic: string; +} + +export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable | Rule | Promise | Promise | void; + +export declare type RuleFactory = (options: T) => Rule; + +export declare function schematic(schematicName: string, options: OptionT, executionOptions?: Partial): Rule; + +export interface Schematic { + readonly collection: Collection; + readonly description: SchematicDescription; + call(options: OptionT, host: Observable, parentContext?: Partial>, executionOptions?: Partial): Observable; +} + +export declare type SchematicContext = TypedSchematicContext<{}, {}>; + +export declare type SchematicDescription = SchematicMetadataT & { + readonly collection: CollectionDescription; + readonly name: string; + readonly private?: boolean; + readonly hidden?: boolean; +}; + +export declare class SchematicEngine implements Engine { + protected _workflow?: Workflow | undefined; + readonly defaultMergeStrategy: MergeStrategy; + readonly workflow: Workflow | null; + constructor(_host: EngineHost, _workflow?: Workflow | undefined); + createCollection(name: string): Collection; + createContext(schematic: Schematic, parent?: Partial>, executionOptions?: Partial): TypedSchematicContext; + createSchematic(name: string, collection: Collection, allowPrivate?: boolean): Schematic; + createSourceFromUrl(url: Url, context: TypedSchematicContext): Source; + executePostTasks(): Observable; + listSchematicNames(collection: Collection): string[]; + transformOptions(schematic: Schematic, options: OptionT, context?: TypedSchematicContext): Observable; +} + +export declare class SchematicEngineConflictingException extends BaseException { + constructor(); +} + +export declare class SchematicImpl implements Schematic { + readonly collection: Collection; + readonly description: SchematicDescription; + constructor(_description: SchematicDescription, _factory: RuleFactory<{}>, _collection: Collection, _engine: Engine); + call(options: OptionT, host: Observable, parentContext?: Partial>, executionOptions?: Partial): Observable; +} + +export declare class SchematicsException extends BaseException { +} + +export declare abstract class SimpleSinkBase implements Sink { + postCommit: () => void | Observable; + postCommitAction: (action: Action) => void | Observable; + preCommit: () => void | Observable; + preCommitAction: (action: Action) => void | Action | PromiseLike | Observable; + protected abstract _createFile(path: string, content: Buffer): Observable; + protected abstract _deleteFile(path: string): Observable; + protected abstract _done(): Observable; + protected _fileAlreadyExistException(path: string): void; + protected _fileDoesNotExistException(path: string): void; + protected abstract _overwriteFile(path: string, content: Buffer): Observable; + protected abstract _renameFile(path: string, to: string): Observable; + protected _validateCreateAction(action: CreateFileAction): Observable; + protected _validateDeleteAction(action: DeleteFileAction): Observable; + protected abstract _validateFileExists(p: string): Observable; + protected _validateOverwriteAction(action: OverwriteFileAction): Observable; + protected _validateRenameAction(action: RenameFileAction): Observable; + commit(tree: Tree): Observable; + commitSingleAction(action: Action): Observable; + validateSingleAction(action: Action): Observable; +} + +export interface Sink { + commit(tree: Tree): Observable; +} + +export declare function source(tree: Tree): Source; + +export declare type Source = (context: SchematicContext) => Tree | Observable; + +export declare const standardFormats: schema.SchemaFormat[]; + +export interface TaskConfiguration { + dependencies?: Array; + name: string; + options?: T; +} + +export interface TaskConfigurationGenerator { + toConfiguration(): TaskConfiguration; +} + +export declare type TaskExecutor = (options: T | undefined, context: SchematicContext) => Promise | Observable; + +export interface TaskExecutorFactory { + readonly name: string; + create(options?: T): Promise | Observable; +} + +export interface TaskId { + readonly id: number; +} + +export interface TaskInfo { + readonly configuration: TaskConfiguration; + readonly context: SchematicContext; + readonly id: number; + readonly priority: number; +} + +export declare class TaskScheduler { + constructor(_context: SchematicContext); + finalize(): ReadonlyArray; + schedule(taskConfiguration: TaskConfiguration): TaskId; +} + +export declare function template(options: T): Rule; + +export declare const TEMPLATE_FILENAME_RE: RegExp; + +export declare type Tree = TreeInterface; + +export interface TreeConstructor { + branch(tree: TreeInterface): TreeInterface; + empty(): TreeInterface; + merge(tree: TreeInterface, other: TreeInterface, strategy?: MergeStrategy): TreeInterface; + optimize(tree: TreeInterface): TreeInterface; + partition(tree: TreeInterface, predicate: FilePredicate): [TreeInterface, TreeInterface]; +} + +export declare const TreeSymbol: symbol; + +export interface TypedSchematicContext { + readonly analytics?: analytics.Analytics; + readonly debug: boolean; + readonly engine: Engine; + readonly interactive: boolean; + readonly logger: logging.LoggerApi; + readonly schematic: Schematic; + readonly strategy: MergeStrategy; + addTask(task: TaskConfigurationGenerator, dependencies?: Array): TaskId; +} + +export declare class UnimplementedException extends BaseException { + constructor(); +} + +export declare class UnknownActionException extends BaseException { + constructor(action: Action); +} + +export declare class UnknownCollectionException extends BaseException { + constructor(name: string); +} + +export declare class UnknownPipeException extends BaseException { + constructor(name: string); +} + +export declare class UnknownSchematicException extends BaseException { + constructor(name: string, collection: CollectionDescription<{}>); +} + +export declare class UnknownTaskDependencyException extends BaseException { + constructor(id: TaskId); +} + +export declare class UnknownUrlSourceProtocol extends BaseException { + constructor(url: string); +} + +export declare class UnregisteredTaskException extends BaseException { + constructor(name: string, schematic?: SchematicDescription<{}, {}>); +} + +export declare class UnsuccessfulWorkflowExecution extends BaseException { + constructor(); +} + +export interface UpdateRecorder { + insertLeft(index: number, content: Buffer | string): UpdateRecorder; + insertRight(index: number, content: Buffer | string): UpdateRecorder; + remove(index: number, length: number): UpdateRecorder; +} + +export declare function url(urlString: string): Source; + +export declare function when(predicate: FilePredicate, operator: FileOperator): FileOperator; + +export interface Workflow { + readonly context: Readonly; + execute(options: Partial & RequiredWorkflowExecutionContext): Observable; +} + +export interface WorkflowExecutionContext extends RequiredWorkflowExecutionContext { + allowPrivate?: boolean; + debug: boolean; + logger: logging.Logger; + parentContext?: Readonly; +} diff --git a/etc/api/angular_devkit/schematics/tasks/index.d.ts b/etc/api/angular_devkit/schematics/tasks/index.d.ts new file mode 100644 index 000000000000..f802db93d246 --- /dev/null +++ b/etc/api/angular_devkit/schematics/tasks/index.d.ts @@ -0,0 +1,42 @@ +export declare class NodePackageInstallTask implements TaskConfigurationGenerator { + packageManager?: string; + packageName?: string; + quiet: boolean; + workingDirectory?: string; + constructor(options: Partial); + constructor(workingDirectory?: string); + toConfiguration(): TaskConfiguration; +} + +export declare class NodePackageLinkTask implements TaskConfigurationGenerator { + packageName?: string | undefined; + quiet: boolean; + workingDirectory?: string | undefined; + constructor(packageName?: string | undefined, workingDirectory?: string | undefined); + toConfiguration(): TaskConfiguration; +} + +export declare class RepositoryInitializerTask implements TaskConfigurationGenerator { + commitOptions?: CommitOptions | undefined; + workingDirectory?: string | undefined; + constructor(workingDirectory?: string | undefined, commitOptions?: CommitOptions | undefined); + toConfiguration(): TaskConfiguration; +} + +export declare class RunSchematicTask implements TaskConfigurationGenerator> { + protected _collection: string | null; + protected _options: T; + protected _schematic: string; + constructor(c: string, s: string, o: T); + constructor(s: string, o: T); + toConfiguration(): TaskConfiguration>; +} + +export declare class TslintFixTask implements TaskConfigurationGenerator { + protected _configOrPath: null | string | JsonObject; + protected _options: TslintFixTaskOptionsBase; + constructor(config: JsonObject, options: TslintFixTaskOptionsBase); + constructor(options: TslintFixTaskOptionsBase); + constructor(path: string, options: TslintFixTaskOptionsBase); + toConfiguration(): TaskConfiguration; +} diff --git a/etc/api/angular_devkit/schematics/testing/index.d.ts b/etc/api/angular_devkit/schematics/testing/index.d.ts new file mode 100644 index 000000000000..14148f616992 --- /dev/null +++ b/etc/api/angular_devkit/schematics/testing/index.d.ts @@ -0,0 +1,17 @@ +export declare class SchematicTestRunner { + readonly engine: SchematicEngine<{}, {}>; + readonly logger: logging.Logger; + readonly tasks: TaskConfiguration[]; + constructor(_collectionName: string, collectionPath: string); + callRule(rule: Rule, tree: Tree, parentContext?: Partial): Observable; + registerCollection(collectionName: string, collectionPath: string): void; + runExternalSchematic(collectionName: string, schematicName: string, opts?: SchematicSchemaT, tree?: Tree): UnitTestTree; + runExternalSchematicAsync(collectionName: string, schematicName: string, opts?: SchematicSchemaT, tree?: Tree): Observable; + runSchematic(schematicName: string, opts?: SchematicSchemaT, tree?: Tree): UnitTestTree; + runSchematicAsync(schematicName: string, opts?: SchematicSchemaT, tree?: Tree): Observable; +} + +export declare class UnitTestTree extends DelegateTree { + readonly files: string[]; + readContent(path: string): string; +} diff --git a/etc/api/angular_devkit/schematics/tools/index.d.ts b/etc/api/angular_devkit/schematics/tools/index.d.ts new file mode 100644 index 000000000000..912d3bedcdd6 --- /dev/null +++ b/etc/api/angular_devkit/schematics/tools/index.d.ts @@ -0,0 +1,163 @@ +export declare class CollectionCannotBeResolvedException extends BaseException { + constructor(name: string); +} + +export declare class CollectionMissingFieldsException extends BaseException { + constructor(name: string); +} + +export declare class CollectionMissingSchematicsMapException extends BaseException { + constructor(name: string); +} + +export declare type ContextTransform = (context: FileSystemSchematicContext) => FileSystemSchematicContext; + +export declare class ExportStringRef { + readonly module: string; + readonly path: string; + readonly ref: T | undefined; + constructor(ref: string, parentPath?: string, inner?: boolean); +} + +export declare class FactoryCannotBeResolvedException extends BaseException { + constructor(name: string); +} + +export declare type FileSystemCollection = Collection; + +export declare type FileSystemCollectionDesc = CollectionDescription; + +export interface FileSystemCollectionDescription { + readonly name: string; + readonly path: string; + readonly schematics: { + [name: string]: FileSystemSchematicDesc; + }; + readonly version?: string; +} + +export declare type FileSystemEngine = Engine; + +export declare class FileSystemEngineHost extends FileSystemEngineHostBase { + protected _root: string; + constructor(_root: string); + protected _resolveCollectionPath(name: string): string; + protected _resolveReferenceString(refString: string, parentPath: string): { + ref: RuleFactory<{}>; + path: string; + } | null; + protected _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + protected _transformSchematicDescription(name: string, _collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; + createTaskExecutor(name: string): Observable; + hasTaskExecutor(name: string): boolean; +} + +export declare abstract class FileSystemEngineHostBase implements FileSystemEngineHost { + protected abstract _resolveCollectionPath(name: string): string; + protected abstract _resolveReferenceString(name: string, parentPath: string): { + ref: RuleFactory<{}>; + path: string; + } | null; + protected abstract _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + protected abstract _transformSchematicDescription(name: string, collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; + createCollectionDescription(name: string): FileSystemCollectionDesc; + createSchematicDescription(name: string, collection: FileSystemCollectionDesc): FileSystemSchematicDesc | null; + createSourceFromUrl(url: Url): Source | null; + createTaskExecutor(name: string): Observable; + getSchematicRuleFactory(schematic: FileSystemSchematicDesc, _collection: FileSystemCollectionDesc): RuleFactory; + hasTaskExecutor(name: string): boolean; + listSchematicNames(collection: FileSystemCollectionDesc): string[]; + listSchematics(collection: FileSystemCollection): string[]; + registerContextTransform(t: ContextTransform): void; + registerOptionsTransform(t: OptionTransform): void; + registerTaskExecutor(factory: TaskExecutorFactory, options?: T): void; + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext; + transformOptions(schematic: FileSystemSchematicDesc, options: OptionT, context?: FileSystemSchematicContext): Observable; +} + +export declare class FileSystemHost extends virtualFs.ScopedHost<{}> { + constructor(dir: string); +} + +export declare type FileSystemSchematic = Schematic; + +export declare type FileSystemSchematicContext = TypedSchematicContext; + +export declare type FileSystemSchematicDesc = SchematicDescription; + +export interface FileSystemSchematicDescription extends FileSystemSchematicJsonDescription { + readonly factoryFn: RuleFactory<{}>; + readonly path: string; + readonly schemaJson?: JsonObject; +} + +export interface FileSystemSchematicJsonDescription { + readonly aliases?: string[]; + readonly collection: FileSystemCollectionDescription; + readonly description: string; + readonly extends?: string; + readonly factory: string; + readonly name: string; + readonly schema?: string; +} + +export declare class InvalidCollectionJsonException extends BaseException { + constructor(_name: string, path: string, jsonException?: UnexpectedEndOfInputException | InvalidJsonCharacterException); +} + +export declare class NodeModulesEngineHost extends FileSystemEngineHostBase { + constructor(); + protected _resolveCollectionPath(name: string): string; + protected _resolvePackageJson(name: string, basedir?: string): string; + protected _resolvePath(name: string, basedir?: string): string; + protected _resolveReferenceString(refString: string, parentPath: string): { + ref: RuleFactory<{}>; + path: string; + } | null; + protected _transformCollectionDescription(name: string, desc: Partial): FileSystemCollectionDesc; + protected _transformSchematicDescription(name: string, _collection: FileSystemCollectionDesc, desc: Partial): FileSystemSchematicDesc; +} + +export declare class NodeModulesTestEngineHost extends NodeModulesEngineHost { + readonly tasks: TaskConfiguration<{}>[]; + protected _resolveCollectionPath(name: string): string; + clearTasks(): void; + registerCollection(name: string, path: string): void; + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext; +} + +export declare class NodePackageDoesNotSupportSchematics extends BaseException { + constructor(name: string); +} + +export declare class NodeWorkflow extends workflow.BaseWorkflow { + readonly engine: FileSystemEngine; + readonly engineHost: NodeModulesEngineHost; + constructor(host: virtualFs.Host, options: { + force?: boolean; + dryRun?: boolean; + root?: Path; + packageManager?: string; + registry?: schema.CoreSchemaRegistry; + }); +} + +export declare type OptionTransform = (schematic: FileSystemSchematicDescription, options: T, context?: FileSystemSchematicContext) => Observable | PromiseLike | R; + +export declare class SchematicMissingDescriptionException extends BaseException { + constructor(name: string); +} + +export declare class SchematicMissingFactoryException extends BaseException { + constructor(name: string); +} + +export declare class SchematicMissingFieldsException extends BaseException { + constructor(name: string); +} + +export declare class SchematicNameCollisionException extends BaseException { + constructor(name: string); +} + +export declare function validateOptionsWithSchema(registry: schema.SchemaRegistry): (schematic: FileSystemSchematicDescription, options: T, context?: import("@angular-devkit/schematics").TypedSchematicContext | undefined) => Observable; diff --git a/etc/cli.angular.io/cli-logo.svg b/etc/cli.angular.io/cli-logo.svg deleted file mode 100644 index fff3d73bfb7c..000000000000 --- a/etc/cli.angular.io/cli-logo.svg +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - image/svg+xml - - - - - - - CLI Copy 2 - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/etc/cli.angular.io/index.html b/etc/cli.angular.io/index.html index c512512f37a4..71f55c419027 100644 --- a/etc/cli.angular.io/index.html +++ b/etc/cli.angular.io/index.html @@ -11,6 +11,7 @@ + @@ -31,16 +32,16 @@ @@ -54,12 +55,24 @@
- Mobile Toolkit +
+
+
+
+
+
+
+ npm install -g @angular/cli + ng new my-dream-app + cd my-dream-app + ng serve +
+

Angular CLI

A command line interface for Angular
- + Get Started
@@ -71,30 +84,30 @@
A command line interface for Angular
- +

ng new

The Angular CLI makes it easy to create an application that already works, right out of the box. It already follows our best practices!

- +

ng generate

Generate components, routes, services and pipes with a simple command. The CLI will also create simple test shells for all of these.

- +

ng serve

Easily test your app locally while developing.

- +
-

Test, Lint, Format

-

Make your code really shine. Run your unittests or your end-to-end tests with the breeze of a command. Execute the official Angular linter and run clang format.

+

Test, Lint

+

Make your code really shine. Run your unit tests, your end-to-end tests, or execute the official Angular linter with the breeze of a command.

- + @@ -113,7 +126,7 @@

Test, Lint, Format

-

Powered by Google ©2010-2016. Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.

+

Powered by Google. Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.

diff --git a/etc/cli.angular.io/main.css b/etc/cli.angular.io/main.css index 06e8032946d9..61b0cb066543 100644 --- a/etc/cli.angular.io/main.css +++ b/etc/cli.angular.io/main.css @@ -1 +1 @@ -.hero-image{margin-top:-10px;width:200px}.hero-image{width:360px;padding-right:40px}@media (max-width:830px){.hero-image{padding-right:0}}.mdl-base{height:100vh}body{font-family:'Roboto',Helvetica,sans-serif}h4{font-size:30px;font-weight:400;line-height:40px;margin-bottom:15px;margin-top:15px}h5{font-size:16px;font-weight:300;line-height:28px;margin-bottom:25px;margin-top:15px;max-width:300px}.mdl-demo section.section--center{max-width:920px}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__header{background-color:rgb(244,67,54);box-shadow:0 2px 5px 0 rgba(0,0,0,0.26)}.mdl-layout__header a{color:rgb(255,255,255);text-decoration:none}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:0;width:100%}.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:25px;padding-right:0}.mdl-layout__drawer-button{display:none}@media (max-width:1024px){.mdl-layout__drawer-button{display:inline-block}}.mdl-layout__drawer{margin-top:65px;height:calc(100% - 65px)}@media (max-width:1024px){.mdl-layout__drawer{margin-top:0;height:100%}}.mdl-layout__title,.mdl-layout-title{font-size:16px;line-height:28px;letter-spacing:0.02em}.microsite-name{display:inline-block;font-size:20px;margin-left:8px;margin-right:30px;text-transform:uppercase;-webkit-transform:translateY(3px);transform:translateY(3px)}.mdl-navigation__link{font-size:16px;text-transform:uppercase;text-decoration:none}.mdl-navigation__link:hover{background-color:rgb(211,47,47)}.top-nav-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.top-nav-wrapper label{display:none}.top-nav-wrapper label:hover{background-color:rgb(211,47,47)}@media (max-width:800px){.top-nav-wrapper{display:block;position:absolute;right:0;top:0;width:100%}.top-nav-wrapper label{cursor:pointer;display:block;float:right;line-height:56px;padding:0 16px}.top-nav-wrapper nav{background:rgb(211,47,47);clear:both;display:none;height:auto!important}.top-nav-wrapper nav a{display:block}.top-nav-wrapper .mdl-layout-spacer{display:none}input:checked+.top-nav-wrapper label{background:rgb(211,47,47)}input:checked+.top-nav-wrapper nav{display:block}}.hero-background{background:-webkit-linear-gradient(rgb(211,47,47),rgb(244,67,54));background:linear-gradient(rgb(211,47,47),rgb(244,67,54));color:rgb(255,255,255);margin-bottom:60px}.mdl-grid{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.hero-container{padding:56px 0 56px 0!important}@media (max-width:830px){.hero-container{text-align:center}}.logo-container{overflow:hidden;text-align:center}@media (max-width:840px){.tagline{max-width:100%}}.mdl-button{height:45px;line-height:45px;min-width:140px;padding:0 30px}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{background-color:rgb(255,255,255);color:rgb(183,28,28)}.features-list{width:920px;margin:0 0 23px 0;padding:15px;padding-right:200px}@media (max-width:840px){.features-list{padding-right:15px}}.features-list h4{color:#37474F;font-size:28px;font-weight:500;line-height:32px;margin-top:10px;margin:0 0 16px 0;opacity:.87}.features-list p{font-size:16px;line-height:30px;opacity:.87}.button-container{margin-bottom:24px!important;text-align:center}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{background-color:rgb(244,67,54);color:rgb(255,255,255)}.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{background-color:#263238;bottom:0;color:white;padding-top:0;right:0}footer ul{font-size:14px;font-weight:400;letter-spacing:0;line-height:24px;list-style:none;padding:0}footer ul a{color:white;font-size:16px;line-height:28px;opacity:.87;padding:0;text-decoration:none}footer ul a:hover{text-decoration:underline}@media (max-width:830px){footer ul{background-color:rgba(0,0,0,0.12);padding:8px;text-align:center}}.mdl-mega-footer--bottom-section{margin-bottom:0}.mdl-mega-footer--bottom-section p{font-size:12px;margin:0;opacity:.54}.mdl-mega-footer--bottom-section a{color:white;font-weight:normal;padding:0;text-decoration:none}.power-text{text-align:right}@media (max-width:830px){.power-text{text-align:center;width:calc(100% - 16px)}}.hero-image{margin-top:-10px;width:200px}.hero-image{width:360px;padding-right:40px}@media (max-width:830px){.hero-image{padding-right:0}}.mdl-base{height:100vh} \ No newline at end of file +body{font-family:"Roboto",Helvetica,sans-serif}h4,h5{font-size:30px;font-weight:400;line-height:40px;margin-bottom:15px;margin-top:15px}h5{font-size:16px;font-weight:300;line-height:28px;margin-bottom:25px;max-width:300px}.mdl-demo section.section--center{max-width:920px}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__header{background-color:#f44336;box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.mdl-layout__header a{color:#fff;text-decoration:none}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:0;width:100%}.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:25px;padding-right:0}.mdl-layout__drawer-button,.top-nav-wrapper label{display:none}@media (max-width:1024px){.mdl-layout__drawer-button{display:inline-block}}.mdl-layout__drawer{margin-top:65px;height:calc(100% - 65px)}@media (max-width:1024px){.mdl-layout__drawer{margin-top:0;height:100%}}.mdl-layout-title,.mdl-layout__title{font-size:16px;line-height:28px;letter-spacing:.02em}.microsite-name{display:inline-block;font-size:20px;margin-left:8px;margin-right:30px;text-transform:uppercase;-webkit-transform:translateY(3px);transform:translateY(3px)}.mdl-navigation__link{font-size:16px;text-transform:uppercase;text-decoration:none}.mdl-navigation__link:hover,.top-nav-wrapper label:hover{background-color:#d32f2f}.top-nav-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media (max-width:800px){.top-nav-wrapper{display:block;position:absolute;right:0;top:0;width:100%}.top-nav-wrapper label{cursor:pointer;display:block;float:right;line-height:56px;padding:0 16px}.top-nav-wrapper nav{background:#d32f2f;clear:both;display:none;height:auto!important}.top-nav-wrapper nav a{display:block}.top-nav-wrapper .mdl-layout-spacer{display:none}input:checked+.top-nav-wrapper label{background:#d32f2f}input:checked+.top-nav-wrapper nav{display:block}}.hero-background{background:-webkit-linear-gradient(#d32f2f ,#f44336);background:linear-gradient(#d32f2f ,#f44336);color:#fff;margin-bottom:60px}.mdl-grid,.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.hero-container{padding:56px 0!important}@media (max-width:830px){.hero-container{text-align:center}}.logo-container{overflow:hidden;text-align:center}@media (max-width:840px){.tagline{max-width:100%}}.mdl-button{height:45px;line-height:45px;min-width:140px;padding:0 30px}.mdl-button--primary.mdl-button--primary.mdl-button--fab,.mdl-button--primary.mdl-button--primary.mdl-button--raised{background-color:#fff;color:#b71c1c}.features-list{width:920px;margin:0 0 23px;padding:15px 200px 15px 15px}@media (max-width:840px){.features-list{padding-right:15px}}.features-list h4{color:#37474f;font-size:28px;font-weight:500;line-height:32px;margin:0 0 16px;opacity:.87}.features-list p,footer ul a{font-size:16px;line-height:30px;opacity:.87}.button-container{margin-bottom:24px!important;text-align:center}.mdl-button--accent.mdl-button--accent.mdl-button--fab,.mdl-button--accent.mdl-button--accent.mdl-button--raised{background-color:#f44336;color:#fff}.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{background-color:#263238;bottom:0;color:#fff;padding-top:0;right:0}footer ul{font-size:14px;font-weight:400;letter-spacing:0;line-height:24px;list-style:none;padding:0}footer ul a{color:#fff;line-height:28px;padding:0;text-decoration:none}footer ul a:hover{text-decoration:underline}@media (max-width:830px){footer ul{background-color:rgba(0,0,0,.12);padding:8px;text-align:center}}.mdl-mega-footer--bottom-section{margin-bottom:0}.mdl-mega-footer--bottom-section p{font-size:12px;margin:0;opacity:.54}.mdl-mega-footer--bottom-section a{color:#fff;font-weight:400;padding:0;text-decoration:none}.power-text{text-align:right}@media (max-width:830px){.power-text{text-align:center;width:calc(100% - 16px)}}.mdl-base{height:100vh} diff --git a/etc/cli.angular.io/theme.css b/etc/cli.angular.io/theme.css index a90faa788171..b6a336e98b0c 100644 --- a/etc/cli.angular.io/theme.css +++ b/etc/cli.angular.io/theme.css @@ -1 +1 @@ -.hero-image{width:360px;padding-right:40px}@media (max-width:830px){.hero-image{padding-right:0}}.mdl-base{height:100vh} \ No newline at end of file +.console{width:360px;max-width:92vw;margin-left:15px;margin-right:40px;text-align:left;border-radius:5px;margin-bottom:10px}@media (max-width:830px){.console{margin-right:auto;margin-left:auto}}.console__head{overflow:hidden;background-color:#d5d5d5;padding:8px 15px;border-top-left-radius:5px;border-top-right-radius:5px}.console__dot{float:left;width:12px;height:12px;border-radius:50%;margin-right:7px;box-shadow:0 1px 1px 0 rgba(0,0,0,.2)}.console__dot--red{background-color:#ff6057}.console__dot--yellow{background-color:#ffc22e}.console__dot--green{background-color:#28ca40}.console__body{background-color:#1e1e1e;padding:30px 17px 20px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.console__prompt{display:block;margin-bottom:15px;font-family:"Source Code Pro",monospace;font-size:15px}.console__prompt::before{content:">";padding-right:15px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-base{height:100vh} diff --git a/etc/rules/README.md b/etc/rules/README.md new file mode 100644 index 000000000000..616923160a2b --- /dev/null +++ b/etc/rules/README.md @@ -0,0 +1,3 @@ +# TsLint Rules + +This folder contains custom TsLint rules specific to this repository. diff --git a/rules/Rule.ts b/etc/rules/Rule.ts similarity index 100% rename from rules/Rule.ts rename to etc/rules/Rule.ts diff --git a/rules/defocusRule.ts b/etc/rules/defocusRule.ts similarity index 100% rename from rules/defocusRule.ts rename to etc/rules/defocusRule.ts diff --git a/rules/importGroupsRule.ts b/etc/rules/importGroupsRule.ts similarity index 100% rename from rules/importGroupsRule.ts rename to etc/rules/importGroupsRule.ts diff --git a/rules/noGlobalTslintDisableRule.ts b/etc/rules/noGlobalTslintDisableRule.ts similarity index 100% rename from rules/noGlobalTslintDisableRule.ts rename to etc/rules/noGlobalTslintDisableRule.ts diff --git a/rules/singleEofLineRule.ts b/etc/rules/singleEofLineRule.ts similarity index 100% rename from rules/singleEofLineRule.ts rename to etc/rules/singleEofLineRule.ts diff --git a/rules/tsconfig.json b/etc/rules/tsconfig.json similarity index 85% rename from rules/tsconfig.json rename to etc/rules/tsconfig.json index deffb2f1791b..dce8cbca6f6f 100644 --- a/rules/tsconfig.json +++ b/etc/rules/tsconfig.json @@ -3,7 +3,7 @@ // things faster. "extends": "../tsconfig.json", "compilerOptions": { - "outDir": "../dist", + "outDir": "../../dist/etc/rules", "baseUrl": "" }, "exclude": [ diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 000000000000..bcb80b40bd17 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,16 @@ +# `/lib` Folder + +This folder includes bootstrap code for the various tools included in this repository. Also +included is the packages meta-information package in `packages.ts`. This is used to read and +understand all the monorepo information (contained in the `.monorepo.json` file, and `package.json` +files across the repo). + +`bootstrap-local.js` should be included when running files from this repository without compiling +first. It allows for compiling and loading packages in memory directly from the repo. Not only +does the `devkit-admin` scripts use this to include the library, but also all binaries linked +locally (like `schematics` and `ng`), when not using the npm published packages. + +`istanbul-local.js` adds global hooks and information to be able to use code coverage with +`bootstrap-local.js`. Istanbul does not keep the sourcemaps properly in sync if they're available +in the code, which happens locally when transpiling TypeScript. It is currently used by the +`test` script and is included by `bootstrap-local.js` for integration. diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js index c64fbe75f723..fd85505c0bef 100644 --- a/lib/bootstrap-local.js +++ b/lib/bootstrap-local.js @@ -7,26 +7,48 @@ */ /* eslint-disable no-console */ 'use strict'; +const debug = require('debug'); +const debugLocal = debug('ng:local'); +const debugBuildEjs = debug('ng:local:build:ejs'); +const debugBuildSchema = debug('ng:local:build:schema'); +const debugBuildTs = debug('ng:local:build:ts'); +const child_process = require('child_process'); const fs = require('fs'); const path = require('path'); +const temp = require('temp'); const ts = require('typescript'); +const tmpRoot = temp.mkdirSync('angular-devkit-'); + +debugLocal('starting bootstrap local'); + +// This processes any extended configs +const compilerOptions = ts.getParsedCommandLineOfConfigFile( + path.join(__dirname, '../tsconfig-test.json'), + { }, + ts.sys, +).options; let _istanbulRequireHook = null; if (process.env['CODE_COVERAGE'] || process.argv.indexOf('--code-coverage') !== -1) { + debugLocal('setup code coverage'); _istanbulRequireHook = require('./istanbul-local').istanbulRequireHook; + // async keyword isn't supported by the Esprima version used by Istanbul version used by us. + // TODO: update istanbul to istanbul-lib-* (see http://istanbul.js.org/) and remove this hack. + compilerOptions.target = 'es2016'; } // Check if we need to profile this CLI run. let profiler = null; if (process.env['DEVKIT_PROFILING']) { + debugLocal('setup profiling'); try { - profiler = require('v8-profiler'); + profiler = require('v8-profiler-node8'); } catch (err) { - throw new Error(`Could not require 'v8-profiler'. You must install it separetely with` + - `'npm install v8-profiler --no-save.\n\nOriginal error:\n\n${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(); @@ -37,6 +59,7 @@ if (process.env['DEVKIT_PROFILING']) { const profileData = JSON.stringify(cpuProfile); const filePath = path.resolve(process.cwd(), process.env.DEVKIT_PROFILING) + '.cpuprofile'; + debugLocal('saving profiling data'); console.log(`Profiling data saved in "${filePath}": ${profileData.length} bytes`); fs.writeFileSync(filePath, profileData); } @@ -52,6 +75,7 @@ if (process.env['DEVKIT_PROFILING']) { } if (process.env['DEVKIT_LONG_STACK_TRACE']) { + debugLocal('setup long stack trace'); Error.stackTraceLimit = Infinity; } @@ -59,32 +83,30 @@ global._DevKitIsLocal = true; global._DevKitRoot = path.resolve(__dirname, '..'); -const compilerOptions = ts.readConfigFile(path.join(__dirname, '../tsconfig.json'), p => { - return fs.readFileSync(p, 'utf-8'); -}).config; - - const oldRequireTs = require.extensions['.ts']; require.extensions['.ts'] = function (m, filename) { // If we're in node module, either call the old hook or simply compile the // file without transpilation. We do not touch node_modules/**. - // We do touch `Angular DevK` files anywhere though. - if (!filename.match(/@angular\/cli\b/) && filename.match(/node_modules/)) { + // To account for Yarn workspaces symlinks, we much check the real path. + if (fs.realpathSync(filename).match(/node_modules/)) { if (oldRequireTs) { return oldRequireTs(m, filename); } - return m._compile(fs.readFileSync(filename), filename); + return m._compile(fs.readFileSync(filename).toString(), filename); } + debugBuildTs(filename); + // Node requires all require hooks to be sync. const source = fs.readFileSync(filename).toString(); try { - let result = ts.transpile(source, compilerOptions['compilerOptions'], filename); + let result = ts.transpile(source, compilerOptions, filename); if (_istanbulRequireHook) { result = _istanbulRequireHook(result, filename); } + debugBuildTs('done'); // Send it to node to execute. return m._compile(result, filename); @@ -97,10 +119,13 @@ require.extensions['.ts'] = function (m, filename) { require.extensions['.ejs'] = function (m, filename) { + debugBuildEjs(filename); + const source = fs.readFileSync(filename).toString(); const template = require('@angular-devkit/core').template; const result = template(source, { sourceURL: filename, sourceMap: true }); + debugBuildEjs('done'); return m._compile(result.source.replace(/return/, 'module.exports.default = '), filename); }; @@ -115,18 +140,65 @@ if (!__dirname.match(new RegExp(`\\${path.sep}node_modules\\${path.sep}`))) { const oldResolve = Module._resolveFilename; Module._resolveFilename = function (request, parent) { + let resolved = null; + let exception; + try { + resolved = oldResolve.call(this, request, parent); + } catch (e) { + exception = e; + } + if (request in packages) { return packages[request].main; } else if (builtinModules.includes(request)) { // It's a native Node module. return oldResolve.call(this, request, parent); + } else if (resolved && resolved.match(/[\\\/]node_modules[\\\/]/)) { + return resolved; } else { const match = Object.keys(packages).find(pkgName => request.startsWith(pkgName + '/')); if (match) { const p = path.join(packages[match].root, request.substr(match.length)); return oldResolve.call(this, p, parent); + } else if (!resolved) { + if (exception) { + throw exception; + } else { + return resolved; + } } else { - return oldResolve.apply(this, arguments); + // Because loading `.ts` ends up AFTER `.json` in the require() logic, requiring a file that has both `.json` + // and `.ts` versions will only get the `.json` content (which wouldn't happen if the .ts was compiled to + // JavaScript). We load `.ts` files first here to avoid this conflict. It's hacky, but so is the rest of this + // file. + const maybeTsPath = resolved.endsWith('.json') && resolved.replace(/\.json$/, '.ts'); + if (maybeTsPath && !request.endsWith('.json')) { + // If the file exists, return its path. If it doesn't, run the quicktype runner on it and return the content. + if (fs.existsSync(maybeTsPath)) { + return maybeTsPath; + } else { + debugBuildSchema('%s', resolved); + + // This script has the be synchronous, so we spawnSync instead of, say, requiring the runner and calling + // the method directly. + const tmpJsonSchemaPath = path.join(tmpRoot, maybeTsPath.replace(/[^0-9a-zA-Z.]/g, '_')); + try { + if (!fs.existsSync(tmpJsonSchemaPath)) { + const quicktypeRunnerPath = path.join(__dirname, '../tools/quicktype_runner.js'); + child_process.spawnSync('node', [quicktypeRunnerPath, resolved, tmpJsonSchemaPath]); + } + + debugBuildSchema('done'); + return tmpJsonSchemaPath; + } catch (_) { + // Just return resolvedPath and let Node deals with it. + console.log(_); + process.exit(99); + } + } + } + + return resolved; } } }; diff --git a/lib/istanbul-local.js b/lib/istanbul-local.js index ef5782661a92..f1bdb1ebfe3e 100644 --- a/lib/istanbul-local.js +++ b/lib/istanbul-local.js @@ -46,7 +46,7 @@ exports.istanbulRequireHook = function(code, filename) { instrumentedCode = instrumentedCode.replace(inlineSourceMapRe, '') + '//# sourceMappingURL=data:application/json;base64,' - + new Buffer(sourceMapGenerator.toString()).toString('base64'); + + Buffer.from(sourceMapGenerator.toString()).toString('base64'); // Keep the consumer from the original source map, because the reports from Istanbul (not // Constantinople) are already mapped against the code. diff --git a/lib/packages.ts b/lib/packages.ts index d418943173c4..4d69ecb81098 100644 --- a/lib/packages.ts +++ b/lib/packages.ts @@ -8,14 +8,12 @@ // tslint:disable-next-line:no-implicit-dependencies import { JsonObject } from '@angular-devkit/core'; import { execSync } from 'child_process'; -import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -const glob = require('glob'); const distRoot = path.join(__dirname, '../dist'); -const { packages: monorepoPackages } = require('../.monorepo.json'); +const { versions: monorepoVersions, packages: monorepoPackages } = require('../.monorepo.json'); export interface PackageInfo { @@ -28,53 +26,20 @@ export interface PackageInfo { build: string; tar: string; private: boolean; + experimental: boolean; packageJson: JsonObject; dependencies: string[]; + reverseDependencies: string[]; snapshot: boolean; snapshotRepo: string; snapshotHash: string; - dirty: boolean; - hash: string; version: string; } export type PackageMap = { [name: string]: PackageInfo }; -const hashCache: {[name: string]: string | null} = {}; -function _getHashOf(pkg: PackageInfo): string { - if (!(pkg.name in hashCache)) { - hashCache[pkg.name] = null; - const md5Stream = crypto.createHash('md5'); - - // Update the stream with all files content. - const files: string[] = glob.sync(path.join(pkg.root, '**'), { nodir: true }); - files.forEach(filePath => { - md5Stream.write(`\0${filePath}\0`); - md5Stream.write(fs.readFileSync(filePath)); - }); - // Update the stream with all versions of upstream dependencies. - pkg.dependencies.forEach(depName => { - md5Stream.write(`\0${depName}\0${_getHashOf(packages[depName])}\0`); - }); - - md5Stream.end(); - - hashCache[pkg.name] = (md5Stream.read() as Buffer).toString('hex'); - } - - const value = hashCache[pkg.name]; - if (!value) { - // Protect against circular dependency. - throw new Error('Circular dependency detected between the following packages: ' - + Object.keys(hashCache).filter(key => hashCache[key] == null).join(', ')); - } - - return value; -} - - function loadPackageJson(p: string) { const root = require('../package.json'); const pkg = require(p); @@ -94,11 +59,11 @@ function loadPackageJson(p: string) { case 'private': case 'workspaces': case 'resolutions': + case 'scripts': continue; // Remove the following keys from the package.json. case 'devDependencies': - case 'scripts': delete pkg[key]; continue; @@ -114,6 +79,14 @@ function loadPackageJson(p: string) { pkg[key] = b; break; + // Overwrite engines to a common default. + case 'engines': + pkg['engines'] = { + 'node': '>= 10.9.0', + 'npm': '>= 6.2.0', + }; + break; + // Overwrite the package's key with to root one. default: pkg[key] = root[key]; @@ -166,16 +139,44 @@ const packageJsonPaths = _findAllPackageJson(path.join(__dirname, '..'), exclude .filter(p => p != path.join(__dirname, '../package.json')); +function _exec(cmd: string) { + return execSync(cmd).toString().trim(); +} + + let gitShaCache: string; function _getSnapshotHash(_pkg: PackageInfo): string { if (!gitShaCache) { - gitShaCache = execSync('git log --format=%h -n1').toString().trim(); + gitShaCache = _exec('git log --format=%h -n1'); } return gitShaCache; } +let stableVersion = ''; +let experimentalVersion = ''; +function _getVersionFromGit(experimental: boolean): string { + if (stableVersion && experimentalVersion) { + return experimental ? experimentalVersion : stableVersion; + } + + const hasLocalChanges = _exec(`git status --porcelain`) != ''; + const scmVersionTagRaw = _exec(`git describe --match v[0-9].[0-9].[0-9]* --abbrev=7 --tags`) + .slice(1); + stableVersion = scmVersionTagRaw.replace(/-([0-9]+)-g/, '+$1.'); + if (hasLocalChanges) { + stableVersion += stableVersion.includes('+') ? '.with-local-changes' : '+with-local-changes'; + } + + experimentalVersion = `0.${stableVersion.replace(/^(\d+)\.(\d+)/, (_, major, minor) => { + return '' + (parseInt(major, 10) * 100 + parseInt(minor, 10)); + })}`; + + return experimental ? experimentalVersion : stableVersion; +} + + // All the supported packages. Go through the packages directory and create a map of // name => PackageInfo. This map is partial as it lacks some information that requires the // map itself to finish building. @@ -205,14 +206,19 @@ export const packages: PackageMap = bin[binName] = p; }); + const experimental = !!packageJson.private || !!packageJson.experimental; + packages[name] = { build: path.join(distRoot, pkgRoot.substr(path.dirname(__dirname).length)), dist: path.join(distRoot, name), root: pkgRoot, relative: path.relative(path.dirname(__dirname), pkgRoot), main: path.resolve(pkgRoot, 'src/index.ts'), - private: packageJson.private, - tar: path.join(distRoot, name.replace('/', '_') + '.tgz'), + private: !!packageJson.private, + experimental, + // yarn doesn't take kindly to @ in tgz filenames + // https://github.com/yarnpkg/yarn/issues/6339 + tar: path.join(distRoot, name.replace(/\/|@/g, '_') + '.tgz'), bin, name, packageJson, @@ -224,9 +230,10 @@ export const packages: PackageMap = }, dependencies: [], - hash: '', - dirty: false, - version: monorepoPackages[name] && monorepoPackages[name].version || '0.0.0', + reverseDependencies: [], + get version() { + return _getVersionFromGit(experimental); + }, }; return packages; @@ -241,13 +248,5 @@ for (const pkgName of Object.keys(packages)) { return name in (pkgJson.dependencies || {}) || name in (pkgJson.devDependencies || {}); }); -} - - -// Update the hash values of each. -for (const pkgName of Object.keys(packages)) { - packages[pkgName].hash = _getHashOf(packages[pkgName]); - if (!monorepoPackages[pkgName] || packages[pkgName].hash != monorepoPackages[pkgName].hash) { - packages[pkgName].dirty = true; - } + pkg.dependencies.forEach(depName => packages[depName].reverseDependencies.push(pkgName)); } diff --git a/package.json b/package.json index 4936df3a1966..30c0eca39302 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "description": "Software Development Kit for Angular", "bin": { "architect": "./bin/architect", + "benchmark": "./bin/benchmark", "build-optimizer": "./bin/build-optimizer", "devkit-admin": "./bin/devkit-admin", "ng": "./bin/ng", - "purify": "./bin/purify", "schematics": "./bin/schematics" }, "keywords": [ @@ -20,12 +20,14 @@ ], "scripts": { "admin": "node ./bin/devkit-admin", + "bazel:format": "find . -type f \\( -name \"*.bzl\" -or -name WORKSPACE -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs buildifier -v --warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default,attr-single-file,constant-glob,ctx-args,depset-iteration,depset-union,dict-concatenation,duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top,native-build,native-package,output-group,package-name,package-on-top,positional-args,redefined-variable,repository-name,same-origin-load,string-iteration,unused-variable", + "bazel:lint": "yarn bazel:format --lint=warn", + "bazel:lint-fix": "yarn bazel:format --lint=fix", + "bazel:test": "bazel test //...", "build": "npm run admin -- build", "build-tsc": "tsc -p tsconfig.json", "fix": "npm run admin -- lint --fix", "lint": "npm run admin -- lint", - "prebuildifier": "bazel build --noshow_progress @com_github_bazelbuild_buildtools//buildifier", - "buildifier": "find . -type f \\( -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/buildifier", "templates": "node ./bin/devkit-admin templates", "test": "node ./bin/devkit-admin test", "test-large": "node ./bin/devkit-admin test --large --spec-reporter", @@ -33,17 +35,16 @@ "test:watch": "nodemon --watch packages -e ts ./bin/devkit-admin test", "validate": "node ./bin/devkit-admin validate", "validate-commits": "./bin/devkit-admin validate-commits", - "prepush": "node ./bin/devkit-admin hooks/pre-push", - "webdriver-update-appveyor": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.37", - "webdriver-update-circleci": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.33" + "preinstall": "node ./tools/yarn/check-yarn.js", + "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.45" }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, "engines": { - "node": ">= 8.9.0", - "npm": ">= 5.5.1" + "node": ">=10.9.0 <13.0.0", + "yarn": ">=1.9.0 <2.0.0" }, "author": "Angular Authors", "license": "MIT", @@ -51,61 +52,99 @@ "url": "https://github.com/angular/angular-cli/issues" }, "homepage": "https://github.com/angular/angular-cli", - "workspaces": [ - "packages/angular/*", - "packages/angular_devkit/*", - "packages/ngtools/*", - "packages/schematics/*" - ], + "workspaces": { + "packages": [ + "packages/angular/*", + "packages/angular_devkit/*", + "packages/ngtools/*", + "packages/schematics/*" + ], + "nohoist": [ + "@angular/compiler-cli" + ] + }, "dependencies": { + "@types/debug": "^4.1.2", + "@types/node-fetch": "^2.1.6", + "@types/progress": "^2.0.3", + "@types/universal-analytics": "^0.4.2", + "@types/uuid": "^3.4.4", + "debug": "^4.1.1", "glob": "^7.0.3", - "temp": "^0.8.3", - "tslint": "^5.11.0", - "typescript": "~2.9.2" + "node-fetch": "^2.2.0", + "prettier": "^1.16.4", + "puppeteer": "1.12.2", + "quicktype-core": "^6.0.15", + "temp": "^0.9.0", + "tslint": "^5.15.0", + "typescript": "3.5.3" }, "devDependencies": { - "@bazel/typescript": "0.16.1", - "@ngtools/json-schema": "^1.1.0", + "@angular/compiler": "~8.2.0-rc.0", + "@angular/compiler-cli": "~8.2.0-rc.0", + "@bazel/bazel": "0.24.1", + "@bazel/buildifier": "^0.22.0", + "@bazel/jasmine": "~0.26.0", + "@bazel/karma": "~0.26.0", + "@bazel/typescript": "~0.26.0", + "@types/browserslist": "^4.4.0", + "@types/caniuse-lite": "^1.0.0", "@types/copy-webpack-plugin": "^4.4.1", "@types/express": "^4.16.0", - "@types/glob": "^5.0.35", + "@types/glob": "^7.0.0", + "@types/inquirer": "^0.0.44", "@types/istanbul": "^0.4.30", - "@types/jasmine": "^2.8.8", + "@types/jasmine": "^3.3.8", + "@types/karma": "^3.0.2", "@types/loader-utils": "^1.1.3", "@types/minimist": "^1.2.0", - "@types/node": "8.10.10", + "@types/node": "10.9.4", "@types/request": "^2.47.1", - "@types/semver": "^5.5.0", + "@types/semver": "^6.0.0", "@types/source-map": "0.5.2", - "@types/webpack": "^4.4.0", - "@types/webpack-dev-server": "^2.9.4", - "@types/webpack-sources": "^0.1.4", + "@types/webpack": "^4.4.11", + "@types/webpack-dev-server": "^3.1.0", + "@types/webpack-sources": "^0.1.5", + "@yarnpkg/lockfile": "1.1.0", + "ajv": "6.10.2", + "ansi-colors": "3.2.4", "common-tags": "^1.8.0", "conventional-changelog": "^1.1.0", "conventional-commits-parser": "^3.0.0", - "gh-got": "^7.0.0", + "gh-got": "^8.0.1", "git-raw-commits": "^2.0.0", - "husky": "^0.14.3", + "husky": "^1.3.1", "istanbul": "^0.4.5", - "jasmine": "^2.6.0", - "jasmine-spec-reporter": "^3.2.0", + "jasmine": "^3.3.1", + "jasmine-spec-reporter": "^4.2.1", + "karma": "~4.2.0", + "karma-jasmine": "^2.0.1", + "karma-jasmine-html-reporter": "^1.4.0", "license-checker": "^20.1.0", "minimatch": "^3.0.4", "minimist": "^1.2.0", - "rxjs": "^6.0.0", - "semver": "^5.3.0", + "npm-registry-client": "8.6.0", + "pacote": "^9.2.3", + "pidtree": "^0.3.0", + "pidusage": "^2.0.17", + "rxjs": "~6.4.0", + "sauce-connect": "https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz", + "semver": "6.3.0", "source-map": "^0.5.6", "source-map-support": "^0.5.0", "spdx-satisfies": "^4.0.0", "tar": "^4.4.4", "through2": "^2.0.3", + "tree-kill": "^1.2.0", + "ts-api-guardian": "0.4.4", "ts-node": "^5.0.0", - "tslint-no-circular-imports": "^0.5.0", - "tslint-sonarts": "^1.7.0" + "tslint-no-circular-imports": "^0.7.0", + "tslint-sonarts": "1.9.0", + "verdaccio": "4.1.0" }, - "resolutions": { - "@types/webpack": "4.4.0", - "@types/webpack-dev-server": "2.9.4", - "rxjs": "6.0.0" + "husky": { + "hooks": { + "pre-push": "node ./bin/devkit-admin hooks/pre-push" + } } } diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 000000000000..82160f26a4f6 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,9 @@ +# `/packages` Folder + +This folder is the root of all defined packages in this repository. + +Packages that are marked as `private: true` will not be published to NPM. These are limited to the +`_` subfolder. + +This folder includes a directory for every scope in NPM, without the `@` sign. Then one folder +per package, which contains the `package.json`. diff --git a/packages/_/benchmark/src/benchmark.ts b/packages/_/benchmark/src/benchmark.ts index c02fc605ee25..7c22abe0136f 100644 --- a/packages/_/benchmark/src/benchmark.ts +++ b/packages/_/benchmark/src/benchmark.ts @@ -15,11 +15,11 @@ declare const global: { const kNanosecondsPerSeconds = 1e9; const kBenchmarkIterationMaxCount = 10000; const kBenchmarkTimeoutInMsec = 5000; -const kWarmupIterationCount = 10; +const kWarmupIterationCount = 100; const kTopMetricCount = 5; -function _run(fn: () => void, collector: number[]) { +function _run(fn: (i: number) => void, collector: number[]) { const timeout = Date.now(); // Gather the first 5 seconds runs, or kMaxNumberOfIterations runs whichever comes first // (soft timeout). @@ -28,7 +28,7 @@ function _run(fn: () => void, collector: number[]) { i++) { // Start time. const start = process.hrtime(); - fn(); + fn(i); // Get the stop difference time. const diff = process.hrtime(start); @@ -41,13 +41,15 @@ function _run(fn: () => void, collector: number[]) { function _stats(metrics: number[]) { metrics.sort((a, b) => a - b); - const middle = metrics.length / 2; + const count = metrics.length; + const middle = count / 2; const mean = Number.isInteger(middle) ? metrics[middle] : ((metrics[middle - 0.5] + metrics[middle + 0.5]) / 2); const total = metrics.reduce((acc, curr) => acc + curr, 0); - const average = total / metrics.length; + const average = total / count; return { + count: count, fastest: metrics.slice(0, kTopMetricCount), slowest: metrics.reverse().slice(0, kTopMetricCount), mean, @@ -56,12 +58,12 @@ function _stats(metrics: number[]) { } -export function benchmark(name: string, fn: () => void, base?: () => void) { +export function benchmark(name: string, fn: (i: number) => void, base?: (i: number) => void) { it(name + ' (time in nanoseconds)', (done) => { process.nextTick(() => { for (let i = 0; i < kWarmupIterationCount; i++) { // Warm it up. - fn(); + fn(i); } const reporter = global.benchmarkReporter; diff --git a/packages/_/devkit/package/schema.json b/packages/_/devkit/package/schema.json index 1689719bd985..4e8bca4c8627 100644 --- a/packages/_/devkit/package/schema.json +++ b/packages/_/devkit/package/schema.json @@ -24,5 +24,9 @@ }, "description": "The human readable name." } - } + }, + "required": [ + "name", + "displayName" + ] } diff --git a/packages/angular/cli/BUILD b/packages/angular/cli/BUILD index 471f0d42475a..6361ada810b6 100644 --- a/packages/angular/cli/BUILD +++ b/packages/angular/cli/BUILD @@ -5,7 +5,8 @@ licenses(["notice"]) # MIT -load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") +load("@npm_bazel_typescript//:defs.bzl", "ts_library") +load("//tools:ts_json_schema.bzl", "ts_json_schema") package(default_visibility = ["//visibility:public"]) @@ -18,17 +19,198 @@ ts_library( "**/*_spec_large.ts", ], ), - data = glob(["**/*.json"]), + data = glob([ + "**/*.json", + "**/*.md", + ]), module_name = "@angular/cli", + # strict_checks = False, deps = [ + ":command_schemas", "//packages/angular_devkit/architect", + "//packages/angular_devkit/architect:node", "//packages/angular_devkit/core", "//packages/angular_devkit/core:node", "//packages/angular_devkit/schematics", "//packages/angular_devkit/schematics:tools", - "@rxjs", - "@rxjs//operators", - # @typings: node - # @typings: semver + # @typings: es2017.object + "@npm//@types/debug", + "@npm//debug", + "@npm//@types/node", + "@npm//@types/inquirer", + "@npm//@types/semver", + "@npm//@types/universal-analytics", + "@npm//@types/uuid", + "@npm//ansi-colors", + "@npm//rxjs", + ], +) + +ts_library( + name = "command_schemas", + srcs = [], + deps = [ + ":add_schema", + ":analytics_schema", + ":build_schema", + ":config_schema", + ":deprecated_schema", + ":doc_schema", + ":e2e_schema", + ":easter_egg_schema", + ":generate_schema", + ":help_schema", + ":lint_schema", + ":new_schema", + ":run_schema", + ":serve_schema", + ":test_schema", + ":update_schema", + ":version_schema", + ":xi18n_schema", + ], +) + +ts_json_schema( + name = "analytics_schema", + src = "commands/analytics.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "add_schema", + src = "commands/add.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "build_schema", + src = "commands/build.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "config_schema", + src = "commands/config.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "deprecated_schema", + src = "commands/deprecated.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "doc_schema", + src = "commands/doc.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "e2e_schema", + src = "commands/e2e.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "easter_egg_schema", + src = "commands/easter-egg.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "generate_schema", + src = "commands/generate.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "help_schema", + src = "commands/help.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "lint_schema", + src = "commands/lint.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "new_schema", + src = "commands/new.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "run_schema", + src = "commands/run.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "serve_schema", + src = "commands/serve.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "test_schema", + src = "commands/test.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "update_schema", + src = "commands/update.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "version_schema", + src = "commands/version.json", + data = [ + "commands/definitions.json", + ], +) + +ts_json_schema( + name = "xi18n_schema", + src = "commands/xi18n.json", + data = [ + "commands/definitions.json", ], ) diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md index b462efe7e56b..fb5082a4d200 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -6,7 +6,7 @@ [![npm](https://img.shields.io/npm/v/%40angular/cli.svg)][npm-badge-url] [![npm](https://img.shields.io/npm/v/%40angular/cli/next.svg)][npm-badge-url] -[![npm](https://img.shields.io/npm/l/@angular/cli.svg)][npm-badge-url] +[![npm](https://img.shields.io/npm/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) @@ -42,10 +42,26 @@ with NPM 5.5.1 or higher. ## Installation **BEFORE YOU INSTALL:** please read the [prerequisites](#prerequisites) + +### Install Globablly ```bash npm install -g @angular/cli ``` +### Install Locally +```bash +npm install @angular/cli +``` + +To run a locally installed version of the angular-cli, you can call `ng` commands directly by adding the `.bin` folder within your local `node_modules` folder to your PATH. The `node_modules` and `.bin` folders are created in the directory where `npm install @angular/cli` was run upon completion of the install command. + +Alternatively, you can install [npx](https://www.npmjs.com/package/npx) and run `npx ng ` within the local directory where `npm install @angular/cli` was run, which will use the locally installed angular-cli. + +### Install Specific Version (Example: 6.1.1) +```bash +npm install -g @angular/cli@6.1.1 +``` + ## Usage ```bash @@ -90,20 +106,20 @@ You can find all possible blueprints in the table below: Scaffold | Usage --- | --- -[Component](https://github.com/angular/angular-cli/wiki/generate-component) | `ng g component my-new-component` -[Directive](https://github.com/angular/angular-cli/wiki/generate-directive) | `ng g directive my-new-directive` -[Pipe](https://github.com/angular/angular-cli/wiki/generate-pipe) | `ng g pipe my-new-pipe` -[Service](https://github.com/angular/angular-cli/wiki/generate-service) | `ng g service my-new-service` -[Class](https://github.com/angular/angular-cli/wiki/generate-class) | `ng g class my-new-class` -[Guard](https://github.com/angular/angular-cli/wiki/generate-guard) | `ng g guard my-new-guard` -[Interface](https://github.com/angular/angular-cli/wiki/generate-interface) | `ng g interface my-new-interface` -[Enum](https://github.com/angular/angular-cli/wiki/generate-enum) | `ng g enum my-new-enum` -[Module](https://github.com/angular/angular-cli/wiki/generate-module) | `ng g module my-module` +[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 this steps: +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` @@ -146,7 +162,7 @@ You can find more details about changes between versions in [the Releases tab on ```bash git clone https://github.com/angular/angular-cli.git -npm install +yarn npm run build cd dist/@angular/cli npm link @@ -210,13 +226,35 @@ Then you can add breakpoints in `dist/@angular` files. For more informations about Node.js debugging in VS Code, see the related [VS Code Documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging). +### CPU Profiling + +In order to investigate performance issues, CPU profiling is often useful. + +To capture a CPU profiling, you can: +1. install the v8-profiler-node8 dependency: `npm install v8-profiler-node8 --no-save` +1. set the NG_CLI_PROFILING Environment variable to the file name you want: + * on Unix systems (Linux & Mac OS X): ̀`export NG_CLI_PROFILING=my-profile` + * on Windows: ̀̀`setx NG_CLI_PROFILING my-profile` + +Then, just run the ng command on which you want to capture a CPU profile. +You will then obtain a `my-profile.cpuprofile` file in the folder from wich you ran the ng command. + +You can use the Chrome Devtools to process it. To do so: +1. open `chrome://inspect/#devices` in Chrome +1. click on "Open dedicated DevTools for Node" +1. go to the "profiler" tab +1. click on the "Load" button and select the generated .cpuprofile file +1. on the left panel, select the associated file + +In addition to this one, another, more elaborated way to capture a CPU profile using the Chrome Devtools is detailed in https://github.com/angular/angular-cli/issues/8259#issue-269908550. + ## Documentation -The documentation for the Angular CLI is located in this repo's [wiki](https://github.com/angular/angular-cli/wiki). +The documentation for the Angular CLI is located in this repo's [wiki](https://angular.io/cli). ## License -MIT +[MIT](https://github.com/angular/angular-cli/blob/master/LICENSE) [travis-badge]: https://travis-ci.org/angular/angular-cli.svg?branch=master @@ -227,4 +265,5 @@ MIT [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 diff --git a/packages/angular/cli/bin/ng b/packages/angular/cli/bin/ng index 7fb032affdcb..b0be0f9d40d5 100755 --- a/packages/angular/cli/bin/ng +++ b/packages/angular/cli/bin/ng @@ -12,10 +12,10 @@ try { // Some older versions of Node do not support let or const. var version = process.version.substr(1).split('.'); -if (Number(version[0]) < 8 || (Number(version[0]) === 8 && Number(version[1]) < 9)) { +if (Number(version[0]) < 10 || (Number(version[0]) === 10 && Number(version[1]) < 9)) { process.stderr.write( - 'You are running version ' + process.version + ' of Node.js, which is not supported by Angular CLI v6.\n' + - 'The official Node.js version that is supported is 8.9 and greater.\n\n' + + 'You are running version ' + process.version + ' of Node.js, which is not supported by Angular CLI 8.0+.\n' + + 'The official Node.js version that is supported is 10.9 or greater.\n\n' + 'Please visit https://nodejs.org/en/ to find instructions on how to update Node.js.\n' ); diff --git a/packages/angular/cli/bin/postinstall/analytics-prompt.js b/packages/angular/cli/bin/postinstall/analytics-prompt.js new file mode 100644 index 000000000000..c1641478d649 --- /dev/null +++ b/packages/angular/cli/bin/postinstall/analytics-prompt.js @@ -0,0 +1,16 @@ +'use strict'; +// This file is ES6 because it needs to be executed as is. + +if ('NG_CLI_ANALYTICS' in process.env) { + return; +} + +(async () => { + try { + const analytics = require('../../models/analytics'); + + if (!analytics.hasGlobalAnalyticsConfiguration()) { + await analytics.promptGlobalAnalytics(); + } + } catch (_) {} +})(); diff --git a/packages/angular/cli/bin/ng-update-message.js b/packages/angular/cli/bin/postinstall/ng-update-message.js similarity index 95% rename from packages/angular/cli/bin/ng-update-message.js rename to packages/angular/cli/bin/postinstall/ng-update-message.js index 81e161911116..5e4742bdefd5 100755 --- a/packages/angular/cli/bin/ng-update-message.js +++ b/packages/angular/cli/bin/postinstall/ng-update-message.js @@ -1,5 +1,5 @@ -#!/usr/bin/env node 'use strict'; +// This file is ES6 because it needs to be executed as is. // Check if the current directory contains '.angular-cli.json'. If it does, show a message to the user that they // should use the migration script. diff --git a/packages/angular/cli/bin/postinstall/script.js b/packages/angular/cli/bin/postinstall/script.js new file mode 100644 index 000000000000..676ede7b84c3 --- /dev/null +++ b/packages/angular/cli/bin/postinstall/script.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +'use strict'; + +require('./ng-update-message'); +require('./analytics-prompt'); diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json index a4a0b9360211..8d3849174445 100644 --- a/packages/angular/cli/commands.json +++ b/packages/angular/cli/commands.json @@ -1,14 +1,14 @@ { "add": "./commands/add.json", + "analytics": "./commands/analytics.json", "build": "./commands/build.json", "config": "./commands/config.json", "doc": "./commands/doc.json", "e2e": "./commands/e2e.json", "make-this-awesome": "./commands/easter-egg.json", - "eject": "./commands/eject.json", "generate": "./commands/generate.json", - "get": "./commands/getset.json", - "set": "./commands/getset.json", + "get": "./commands/deprecated.json", + "set": "./commands/deprecated.json", "help": "./commands/help.json", "lint": "./commands/lint.json", "new": "./commands/new.json", diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index ba33b8bcc95f..72370fa873a7 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -5,89 +5,181 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any -import { tags, terminal } from '@angular-devkit/core'; +import { analytics, tags } from '@angular-devkit/core'; import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; -import { parseOptions } from '../models/command-runner'; -import { SchematicCommand } from '../models/schematic-command'; -import { NpmInstall } from '../tasks/npm-install'; -import { getPackageManager } from '../utilities/config'; +import { dirname, join } from 'path'; +import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver'; +import { isPackageNameSafeForAnalytics } from '../models/analytics'; +import { Arguments } from '../models/interface'; +import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command'; +import npmInstall from '../tasks/npm-install'; +import { colors } from '../utilities/color'; +import { getPackageManager } from '../utilities/package-manager'; +import { + PackageManifest, + fetchPackageManifest, + fetchPackageMetadata, +} from '../utilities/package-metadata'; +import { Schema as AddCommandSchema } from './add'; +const npa = require('npm-package-arg'); -export class AddCommand extends SchematicCommand { +export class AddCommand extends SchematicCommand { readonly allowPrivateSchematics = true; + readonly allowAdditionalArgs = true; + readonly packageManager = getPackageManager(this.workspace.root); - private async _parseSchematicOptions(collectionName: string): Promise { - const schematicOptions = await this.getOptions({ - schematicName: 'ng-add', - collectionName, - }); - this.addOptions(schematicOptions); + async run(options: AddCommandSchema & Arguments) { + if (!options.collection) { + this.logger.fatal( + `The "ng add" command requires a name argument to be specified eg. ` + + `${colors.yellow('ng add [name] ')}. For more details, use "ng help".`, + ); - return parseOptions(this._rawArgs, this.options); - } + return 1; + } - validate(options: any) { - const collectionName = options._[0]; + let packageIdentifier; + try { + packageIdentifier = npa(options.collection); + } catch (e) { + this.logger.error(e.message); - if (!collectionName) { - this.logger.fatal( - `The "ng add" command requires a name argument to be specified eg. ` - + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); + return 1; + } - return false; + if (packageIdentifier.registry && this.isPackageInstalled(packageIdentifier.name)) { + // Already installed so just run schematic + this.logger.info('Skipping installation: Package already installed'); + + return this.executeSchematic(packageIdentifier.name, options['--']); } - return true; - } + const usingYarn = this.packageManager === 'yarn'; - async run(options: any) { - const firstArg = options._[0]; + if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) { + // only package name provided; search for viable version + // plus special cases for packages that did not have peer deps setup + let packageMetadata; + try { + packageMetadata = await fetchPackageMetadata(packageIdentifier.name, this.logger, { + registry: options.registry, + usingYarn, + verbose: options.verbose, + }); + } catch (e) { + this.logger.error('Unable to fetch package metadata: ' + e.message); - if (!firstArg) { - this.logger.fatal( - `The "ng add" command requires a name argument to be specified eg. ` - + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); + return 1; + } - return 1; + const latestManifest = packageMetadata.tags['latest']; + if (latestManifest && Object.keys(latestManifest.peerDependencies).length === 0) { + if (latestManifest.name === '@angular/pwa') { + const version = await this.findProjectVersion('@angular/cli'); + // tslint:disable-next-line:no-any + const semverOptions = { includePrerelease: true } as any; + + if ( + version && + ((validRange(version) && intersects(version, '7', semverOptions)) || + (valid(version) && satisfies(version, '7', semverOptions))) + ) { + packageIdentifier = npa.resolve('@angular/pwa', '0.12'); + } + } + } else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) { + // 'latest' is invalid so search for most recent matching package + const versionManifests = Array.from(packageMetadata.versions.values()).filter( + value => !prerelease(value.version), + ); + + versionManifests.sort((a, b) => rcompare(a.version, b.version, true)); + + let newIdentifier; + for (const versionManifest of versionManifests) { + if (!(await this.hasMismatchedPeer(versionManifest))) { + newIdentifier = npa.resolve(packageIdentifier.name, versionManifest.version); + break; + } + } + + if (!newIdentifier) { + this.logger.warn("Unable to find compatible package. Using 'latest'."); + } else { + packageIdentifier = newIdentifier; + } + } + } + + let collectionName = packageIdentifier.name; + if (!packageIdentifier.registry) { + try { + const manifest = await fetchPackageManifest(packageIdentifier, this.logger, { + registry: options.registry, + verbose: options.verbose, + usingYarn, + }); + + collectionName = manifest.name; + + if (await this.hasMismatchedPeer(manifest)) { + this.logger.warn( + 'Package has unmet peer dependencies. Adding the package may not succeed.', + ); + } + } catch (e) { + this.logger.error('Unable to fetch package manifest: ' + e.message); + + return 1; + } } - const packageManager = getPackageManager(); + await npmInstall(packageIdentifier.raw, this.logger, this.packageManager, this.workspace.root); + + return this.executeSchematic(collectionName, options['--']); + } + + async reportAnalytics( + paths: string[], + options: AddCommandSchema & Arguments, + dimensions: (boolean | number | string)[] = [], + metrics: (boolean | number | string)[] = [], + ): Promise { + const collection = options.collection; - const npmInstall: NpmInstall = require('../tasks/npm-install').default; + // Add the collection if it's safe listed. + if (collection && isPackageNameSafeForAnalytics(collection)) { + dimensions[analytics.NgCliAnalyticsDimensions.NgAddCollection] = collection; + } else { + delete dimensions[analytics.NgCliAnalyticsDimensions.NgAddCollection]; + } - const packageName = firstArg.startsWith('@') - ? firstArg.split('/', 2).join('/') - : firstArg.split('/', 1)[0]; + return super.reportAnalytics(paths, options, dimensions, metrics); + } - // Remove the tag/version from the package name. - const collectionName = ( - packageName.startsWith('@') - ? packageName.split('@', 2).join('@') - : packageName.split('@', 1).join('@') - ) + firstArg.slice(packageName.length); + private isPackageInstalled(name: string): boolean { + try { + require.resolve(join(name, 'package.json'), { paths: [this.workspace.root] }); - // We don't actually add the package to package.json, that would be the work of the package - // itself. - await npmInstall( - packageName, - this.logger, - packageManager, - this.project.root, - ); + return true; + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } - // Reparse the options with the new schematic accessible. - options = await this._parseSchematicOptions(collectionName); + return false; + } - const runOptions = { + private async executeSchematic( + collectionName: string, + options: string[] = [], + ): Promise { + const runOptions: RunSchematicOptions = { schematicOptions: options, - workingDir: this.project.root, collectionName, schematicName: 'ng-add', - allowPrivate: true, dryRun: false, force: false, }; @@ -107,4 +199,74 @@ export class AddCommand extends SchematicCommand { throw e; } } + + private async findProjectVersion(name: string): Promise { + let installedPackage; + try { + installedPackage = require.resolve(join(name, 'package.json'), { + paths: [this.workspace.root], + }); + } catch {} + + if (installedPackage) { + try { + const installed = await fetchPackageManifest(dirname(installedPackage), this.logger); + + return installed.version; + } catch {} + } + + let projectManifest; + try { + projectManifest = await fetchPackageManifest(this.workspace.root, this.logger); + } catch {} + + if (projectManifest) { + const version = projectManifest.dependencies[name] || projectManifest.devDependencies[name]; + if (version) { + return version; + } + } + + return null; + } + + private async hasMismatchedPeer(manifest: PackageManifest): Promise { + for (const peer in manifest.peerDependencies) { + let peerIdentifier; + try { + peerIdentifier = npa.resolve(peer, manifest.peerDependencies[peer]); + } catch { + this.logger.warn(`Invalid peer dependency ${peer} found in package.`); + continue; + } + + if (peerIdentifier.type === 'version' || peerIdentifier.type === 'range') { + try { + const version = await this.findProjectVersion(peer); + if (!version) { + continue; + } + + // tslint:disable-next-line:no-any + const options = { includePrerelease: true } as any; + + if ( + !intersects(version, peerIdentifier.rawSpec, options) && + !satisfies(version, peerIdentifier.rawSpec, options) + ) { + return true; + } + } catch { + // Not found or invalid so ignore + continue; + } + } else { + // type === 'tag' | 'file' | 'directory' | 'remote' | 'git' + // Cannot accurately compare these as the tag/location may have changed since install + } + } + + return false; + } } diff --git a/packages/angular/cli/commands/add.json b/packages/angular/cli/commands/add.json index 04a9f3a888ed..45679b0faf4d 100644 --- a/packages/angular/cli/commands/add.json +++ b/packages/angular/cli/commands/add.json @@ -1,23 +1,50 @@ { "$schema": "http://json-schema.org/schema", - "id": "AddCommandOptions", - "description": "Add support for a library to your project.", - "$longDescription": "", + "$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", - "properties": { - "collection": { - "type": "string", - "description": "The package to be added.", - "$default": { - "$source": "argv", - "index": 0 - } + "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 + } + }, + "required": [ + ] + }, + { + "$ref": "./definitions.json#/definitions/interactive" + }, + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/add.md b/packages/angular/cli/commands/add.md new file mode 100644 index 000000000000..d2bbcd6af692 --- /dev/null +++ b/packages/angular/cli/commands/add.md @@ -0,0 +1,8 @@ +Adds the npm package for a published library to your workspace, and configures your default +app project to use that library, in whatever way is specified by the library's schematic. +For example, adding `@angular/pwa` configures your project for PWA support: +```bash +ng add @angular/pwa +``` + +The default app project is the value of `defaultProject` in `angular.json`. diff --git a/packages/angular/cli/commands/analytics-impl.ts b/packages/angular/cli/commands/analytics-impl.ts new file mode 100644 index 000000000000..98209f5207ab --- /dev/null +++ b/packages/angular/cli/commands/analytics-impl.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + promptGlobalAnalytics, + promptProjectAnalytics, + setAnalyticsConfig, +} from '../models/analytics'; +import { Command } from '../models/command'; +import { Arguments } from '../models/interface'; +import { ProjectSetting, Schema as AnalyticsCommandSchema, SettingOrProject } from './analytics'; + + +export class AnalyticsCommand extends Command { + public async run(options: AnalyticsCommandSchema & Arguments) { + // Our parser does not support positional enums (won't report invalid parameters). Do the + // validation manually. + // TODO(hansl): fix parser to better support positionals. This would be a breaking change. + if (options.settingOrProject === undefined) { + if (options['--']) { + // The user passed positional arguments but they didn't validate. + this.logger.error(`Argument ${JSON.stringify(options['--'][0])} is invalid.`); + this.logger.error(`Please provide one of the following value: on, off, ci or project.`); + + return 1; + } else { + // No argument were passed. + await this.printHelp(options); + + return 2; + } + } else if (options.settingOrProject == SettingOrProject.Project + && options.projectSetting === undefined) { + this.logger.error(`Argument ${JSON.stringify(options.settingOrProject)} requires a second ` + + `argument of one of the following value: on, off.`); + + return 2; + } + + try { + switch (options.settingOrProject) { + case SettingOrProject.Off: + setAnalyticsConfig('global', false); + break; + + case SettingOrProject.On: + setAnalyticsConfig('global', true); + break; + + case SettingOrProject.Ci: + setAnalyticsConfig('global', 'ci'); + break; + + case SettingOrProject.Project: + switch (options.projectSetting) { + case ProjectSetting.Off: + setAnalyticsConfig('local', false); + break; + + case ProjectSetting.On: + setAnalyticsConfig('local', true); + break; + + case ProjectSetting.Prompt: + await promptProjectAnalytics(true); + break; + + default: + await this.printHelp(options); + + return 3; + } + break; + + case SettingOrProject.Prompt: + await promptGlobalAnalytics(true); + break; + + default: + await this.printHelp(options); + + return 4; + } + } catch (err) { + this.logger.fatal(err.message); + + return 1; + } + + return 0; + } +} diff --git a/packages/angular/cli/commands/analytics-long.md b/packages/angular/cli/commands/analytics-long.md new file mode 100644 index 000000000000..60e0d86686e8 --- /dev/null +++ b/packages/angular/cli/commands/analytics-long.md @@ -0,0 +1,7 @@ +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*. \ No newline at end of file diff --git a/packages/angular/cli/commands/analytics.json b/packages/angular/cli/commands/analytics.json new file mode 100644 index 000000000000..f806e5c8e7bf --- /dev/null +++ b/packages/angular/cli/commands/analytics.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "ng-cli://commands/analytics.json", + "description": "Configures the gathering of Angular CLI usage metrics. See https://v8.angular.io/cli/usage-analytics-gathering.", + "$longDescription": "./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 index 60b82df73e29..081588c2b9cb 100644 --- a/packages/angular/cli/commands/build-impl.ts +++ b/packages/angular/cli/commands/build-impl.ts @@ -5,22 +5,28 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - +import { analytics } from '@angular-devkit/core'; import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Version } from '../upgrade/version'; +import { Arguments } from '../models/interface'; +import { Schema as BuildCommandSchema } from './build'; -export class BuildCommand extends ArchitectCommand { +export class BuildCommand extends ArchitectCommand { public readonly target = 'build'; - public validate(options: ArchitectCommandOptions) { - // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); - - return super.validate(options); + public async run(options: ArchitectCommandOptions & Arguments) { + return this.runArchitectTarget(options); } - public async run(options: ArchitectCommandOptions) { - return this.runArchitectTarget(options); + async reportAnalytics( + paths: string[], + options: BuildCommandSchema & Arguments, + dimensions: (boolean | number | string)[] = [], + metrics: (boolean | number | string)[] = [], + ): Promise { + if (options.buildEventLog !== undefined) { + dimensions[analytics.NgCliAnalyticsDimensions.NgBuildBuildEventLog] = true; + } + + return super.reportAnalytics(paths, options, dimensions, metrics); } } diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build-long.md new file mode 100644 index 000000000000..4e10498f4cd5 --- /dev/null +++ b/packages/angular/cli/commands/build-long.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`, and `watch` options are applied. +All other options apply only to building applications. + +The application builder uses the [webpack](https://webpack.js.org/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration. +A "production" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration="production"` or the `--prod="true"` option. + +The configuration options generally correspond to the command options. +You can override individual configuration defaults by specifying the corresponding options on the command line. +The command can accept option names given in either dash-case or camelCase. +Note that in the configuration file, you must specify names in camelCase. + +Some additional options can only be set through the configuration file, +either by direct editing or with the `ng config` command. +These include `assets`, `styles`, and `scripts` objects that provide runtime-global resources to include in the project. +Resources in CSS, such as images and fonts, are automatically written and fingerprinted at the root of the output folder. + +For further details, see [Workspace Configuration](guide/workspace-config). diff --git a/packages/angular/cli/commands/build.json b/packages/angular/cli/commands/build.json index 247914d3ba3f..6678ba5d744e 100644 --- a/packages/angular/cli/commands/build.json +++ b/packages/angular/cli/commands/build.json @@ -1,34 +1,25 @@ { "$schema": "http://json-schema.org/schema", - "id": "BuildCommandOptions", - "description": "Builds your app and places it into the output path (dist/ by default).", - "$longDescription": "", + "$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", - "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to build.", - "$default": { - "$source": "argv", - "index": 0 + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" }, + { + "type": "object", + "properties": { + "buildEventLog": { + "type": "string", + "description": "**EXPERIMENTAL** Output file path for Build Event Protocol events" + } } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts index b1850b43999b..f69e5f024dc1 100644 --- a/packages/angular/cli/commands/config-impl.ts +++ b/packages/angular/cli/commands/config-impl.ts @@ -17,26 +17,68 @@ import { tags, } from '@angular-devkit/core'; import { writeFileSync } from 'fs'; +import { v4 as uuidV4 } from 'uuid'; import { Command } from '../models/command'; +import { Arguments, CommandScope } from '../models/interface'; import { getWorkspace, getWorkspaceRaw, migrateLegacyGlobalConfig, validateWorkspace, } from '../utilities/config'; +import { Schema as ConfigCommandSchema, Value as ConfigCommandSchemaValue } from './config'; -export interface ConfigOptions { - jsonPath: string; - value?: string; - global?: boolean; +function _validateBoolean(value: string) { + if (('' + value).trim() === 'true') { + return true; + } else if (('' + value).trim() === 'false') { + return false; + } else { + throw new Error(`Invalid value type; expected Boolean, received ${JSON.stringify(value)}.`); + } +} +function _validateNumber(value: string) { + const numberValue = Number(value); + if (!Number.isFinite(numberValue)) { + return numberValue; + } + throw new Error(`Invalid value type; expected Number, received ${JSON.stringify(value)}.`); +} +function _validateString(value: string) { + return value; +} +function _validateAnalytics(value: string) { + if (value === '') { + // Disable analytics. + return null; + } else { + return value; + } } +function _validateAnalyticsSharingUuid(value: string) { + if (value == '') { + return uuidV4(); + } else { + return value; + } +} +function _validateAnalyticsSharingTracking(value: string) { + if (!value.match(/^GA-\d+-\d+$/)) { + throw new Error(`Invalid GA property ID: ${JSON.stringify(value)}.`); + } -const validCliPaths = new Map([ - ['cli.warnings.versionMismatch', 'boolean'], - ['cli.warnings.typescriptMismatch', 'boolean'], - ['cli.defaultCollection', 'string'], - ['cli.packageManager', 'string'], + return value; +} + + +const validCliPaths = new Map JsonValue)>([ + ['cli.warnings.versionMismatch', _validateBoolean], + ['cli.defaultCollection', _validateString], + ['cli.packageManager', _validateString], + ['cli.analytics', _validateAnalytics], + ['cli.analyticsSharing.tracking', _validateAnalyticsSharingTracking], + ['cli.analyticsSharing.uuid', _validateAnalyticsSharingUuid], ]); /** @@ -44,12 +86,12 @@ const validCliPaths = new Map([ * by the path. For example, a path of "a[3].foo.bar[2]" would give you a fragment array of * ["a", 3, "foo", "bar", 2]. * @param path The JSON string to parse. - * @returns {string[]} The fragments for the string. + * @returns {(string|number)[]} The fragments for the string. * @private */ -function parseJsonPath(path: string): string[] { +function parseJsonPath(path: string): (string|number)[] { const fragments = (path || '').split(/\./g); - const result: string[] = []; + const result: (string|number)[] = []; while (fragments.length > 0) { const fragment = fragments.shift(); @@ -64,12 +106,15 @@ function parseJsonPath(path: string): string[] { result.push(match[1]); if (match[2]) { - const indices = match[2].slice(1, -1).split(']['); + 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); + return result.filter(fragment => fragment != null); } function getValueFromPath( @@ -139,28 +184,10 @@ function setValueFromPath( } } -function normalizeValue(value: string, path: string): JsonValue { +function normalizeValue(value: ConfigCommandSchemaValue, path: string): JsonValue { const cliOptionType = validCliPaths.get(path); if (cliOptionType) { - switch (cliOptionType) { - case 'boolean': - if (value.trim() === 'true') { - return true; - } else if (value.trim() === 'false') { - return false; - } - break; - case 'number': - const numberValue = Number(value); - if (!Number.isNaN(numberValue)) { - return numberValue; - } - break; - case 'string': - return value; - } - - throw new Error(`Invalid value type; expected a ${cliOptionType}.`); + return cliOptionType('' + value); } if (typeof value === 'string') { @@ -178,10 +205,14 @@ function normalizeValue(value: string, path: string): JsonValue { return value; } -export class ConfigCommand extends Command { - public run(options: ConfigOptions) { +export class ConfigCommand extends Command { + public async run(options: ConfigCommandSchema & Arguments) { const level = options.global ? 'global' : 'local'; + if (!options.global) { + await this.validateScope(CommandScope.InProject); + } + let config = (getWorkspace(level) as {} as { _workspace: experimental.workspace.WorkspaceSchema }); @@ -210,9 +241,18 @@ export class ConfigCommand extends Command { } } - private get(config: experimental.workspace.WorkspaceSchema, options: ConfigOptions) { + private get(config: experimental.workspace.WorkspaceSchema, options: ConfigCommandSchema) { let value; if (options.jsonPath) { + if (options.jsonPath === 'cli.warnings.typescriptMismatch') { + // NOTE: Remove this in 9.0. + this.logger.warn('The "typescriptMismatch" warning has been removed in 8.0.'); + // Since there is no actual warning, this value is always false. + this.logger.info('false'); + + return 0; + } + value = getValueFromPath(config as {} as JsonObject, options.jsonPath); } else { value = config; @@ -227,12 +267,22 @@ export class ConfigCommand extends Command { } else { this.logger.info(value.toString()); } + + return 0; } - private set(options: ConfigOptions) { + private set(options: ConfigCommandSchema) { if (!options.jsonPath || !options.jsonPath.trim()) { throw new Error('Invalid Path.'); } + + if (options.jsonPath === 'cli.warnings.typescriptMismatch') { + // NOTE: Remove this in 9.0. + this.logger.warn('The "typescriptMismatch" warning has been removed in 8.0.'); + + return 0; + } + if (options.global && !options.jsonPath.startsWith('schematics.') && !validCliPaths.has(options.jsonPath)) { @@ -268,6 +318,8 @@ export class ConfigCommand extends Command { const output = JSON.stringify(configValue, null, 2); writeFileSync(configPath, output); + + return 0; } } diff --git a/packages/angular/cli/commands/config-long.md b/packages/angular/cli/commands/config-long.md new file mode 100644 index 000000000000..5f6052b3894a --- /dev/null +++ b/packages/angular/cli/commands/config-long.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 in either camelCase or dash-case. + +For further details, see [Workspace Configuration](guide/workspace-config). + +For configuration of CLI usage analytics, see [Gathering an Viewing CLI Usage Analytics](./usage-analytics-gathering). \ No newline at end of file diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json index 0caaa4f5415a..d0c3d59520b3 100644 --- a/packages/angular/cli/commands/config.json +++ b/packages/angular/cli/commands/config.json @@ -1,39 +1,44 @@ { "$schema": "http://json-schema.org/schema", - "id": "ConfigCommandOptions", - "description": "Get/set configuration values.", + "$id": "ng-cli://commands/config.json", + "description": "Retrieves or sets Angular configuration values in the angular.json file for the workspace.", "$longDescription": "", "$aliases": [], - "$scope": "in", + "$scope": "all", "$type": "native", "$impl": "./config-impl#ConfigCommand", "type": "object", - "properties": { - "jsonPath": { - "type": "string", - "description": "The path to the value to get/set.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "jsonPath": { + "type": "string", + "description": "The configuration key to set or query, in JSON path format. For example: \"a[3].foo.bar[2]\". If no new value is provided, returns the current value of this key.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "value": { + "type": ["string", "number", "boolean"], + "description": "If provided, a new value for the given configuration key.", + "$default": { + "$source": "argv", + "index": 1 + } + }, + "global": { + "type": "boolean", + "description": "When true, accesses the global configuration in the caller's home directory.", + "default": false, + "aliases": ["g"] + } + }, + "required": [ + ] }, - "value": { - "type": "string", - "description": "The new value to be set.", - "$default": { - "$source": "argv", - "index": 1 - } - }, - "global": { - "type": "boolean", - "description": "Get/set the value in the global configuration (in your home directory).", - "default": false, - "aliases": ["g"] - } - }, - "required": [ + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json new file mode 100644 index 000000000000..1713520fc1d1 --- /dev/null +++ b/packages/angular/cli/commands/definitions.json @@ -0,0 +1,70 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "ng-cli://commands/definitions.json", + + "definitions": { + "architect": { + "properties": { + "project": { + "type": "string", + "description": "The name of the project to build. Can be an application or a library.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "A named build target, as specified in the \"configurations\" section of angular.json.\nEach named target is accompanied by a configuration of option defaults for that target.\nSetting this explicitly overrides the \"--prod\" flag", + "type": "string", + "aliases": [ + "c" + ] + }, + "prod": { + "description": "Shorthand for \"--configuration=production\".\nWhen true, sets the build configuration to the production target.\nBy default, the production target is set up in the workspace configuration such that all builds make use of bundling, limited tree-shaking, and also limited dead code elimination.", + "type": "boolean" + } + } + }, + "base": { + "type": "object", + "properties": { + "help": { + "enum": [true, false, "json", "JSON"], + "description": "Shows a help message for this command in the console.", + "default": false + } + } + }, + "schematic": { + "properties": { + "dryRun": { + "type": "boolean", + "default": false, + "aliases": [ "d" ], + "description": "When true, runs through and reports activity without writing out results." + }, + "force": { + "type": "boolean", + "default": false, + "aliases": [ "f" ], + "description": "When true, forces overwriting of existing files." + } + } + }, + "interactive": { + "properties": { + "interactive": { + "type": "boolean", + "default": "true", + "description": "When false, disables interactive input prompts." + }, + "defaults": { + "type": "boolean", + "default": "false", + "description": "When true, disables interactive input prompts for options with a default." + } + } + } + } +} diff --git a/packages/angular/cli/commands/deprecated-impl.ts b/packages/angular/cli/commands/deprecated-impl.ts new file mode 100644 index 000000000000..7887eac8d04d --- /dev/null +++ b/packages/angular/cli/commands/deprecated-impl.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Command } from '../models/command'; +import { Schema as DeprecatedCommandSchema } from './deprecated'; + +export class DeprecatedCommand extends Command { + public async run() { + let message = 'The "${this.description.name}" command has been deprecated.'; + if (this.description.name == 'get' || this.description.name == 'set') { + message = 'get/set have been deprecated in favor of the config command.'; + } + + this.logger.error(message); + + return 0; + } +} diff --git a/packages/angular/cli/commands/deprecated.json b/packages/angular/cli/commands/deprecated.json new file mode 100644 index 000000000000..05dd86855d95 --- /dev/null +++ b/packages/angular/cli/commands/deprecated.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "ng-cli://commands/deprecated.json", + "description": "Deprecated in favor of config command.", + "$longDescription": "", + + "$impl": "./deprecated-impl#DeprecatedCommand", + "$hidden": true, + "$type": "deprecated", + + "type": "object", + "allOf": [ + { "$ref": "./definitions.json#/definitions/base" } + ] +} diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts index b9f6c9451fe4..2330f9c10005 100644 --- a/packages/angular/cli/commands/doc-impl.ts +++ b/packages/angular/cli/commands/doc-impl.ts @@ -7,30 +7,54 @@ */ import { Command } from '../models/command'; -const opn = require('opn'); +import { Arguments } from '../models/interface'; +import { Schema as DocCommandSchema } from './doc'; -export interface Options { - keyword: string; - search?: boolean; -} +const open = require('open'); -export class DocCommand extends Command { - public validate(options: Options) { +export class DocCommand extends Command { + public async run(options: DocCommandSchema & Arguments) { if (!options.keyword) { - this.logger.error(`keyword argument is required.`); + this.logger.error('You should specify a keyword, for instance, `ng doc ActivatedRoute`.'); - return false; + return 0; } - return true; - } + let domain = 'angular.io'; + + if (options.version) { + // version can either be a string containing "next" + if (options.version == 'next') { + domain = 'next.angular.io'; + // or a number where version must be a valid Angular version (i.e. not 0, 1 or 3) + } else if (!isNaN(+options.version) && ![0, 1, 3].includes(+options.version)) { + domain = `v${options.version}.angular.io`; + } else { + this.logger.error('Version should either be a number (2, 4, 5, 6...) or "next"'); + + return 0; + } + } else { + // we try to get the current Angular version of the project + // and use it if we can find it + try { + /* tslint:disable-next-line:no-implicit-dependencies */ + const currentNgVersion = require('@angular/core').VERSION.major; + domain = `v${currentNgVersion}.angular.io`; + } catch (e) {} + } + + let searchUrl = `https://${domain}/api?query=${options.keyword}`; - public async run(options: Options) { - let searchUrl = `https://angular.io/api?query=${options.keyword}`; if (options.search) { - searchUrl = `https://www.google.com/search?q=site%3Aangular.io+${options.keyword}`; + searchUrl = `https://www.google.com/search?q=site%3A${domain}+${options.keyword}`; } - return opn(searchUrl); + // We should wrap `open` in a new Promise because `open` is already resolved + await new Promise(() => { + open(searchUrl, { + wait: false, + }); + }); } } diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json index d0fd2ce0c4fa..b43f448d03cb 100644 --- a/packages/angular/cli/commands/doc.json +++ b/packages/angular/cli/commands/doc.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "id": "DocCommandOptions", - "description": "Opens the official Angular API documentation for a given keyword.", + "$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" ], @@ -9,22 +9,39 @@ "$impl": "./doc-impl#DocCommand", "type": "object", - "properties": { - "keyword": { - "type": "string", - "description": "The query to search upon.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "keyword": { + "type": "string", + "description": "The keyword to search for, as provided in the search bar in angular.io.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "search": { + "aliases": ["s"], + "type": "boolean", + "default": false, + "description": "When true, searches all of angular.io. Otherwise, searches only API reference documentation." + }, + "version" : { + "oneOf": [ + { + "type": "number", + "minimum": 4 + }, + { + "enum": [2, "next"] + } + ], + "description": "Contains the version of Angular to use for the documentation. If not provided, the command uses your current Angular core version." + } + }, + "required": [ + ] }, - "search": { - "aliases": ["s"], - "type": "boolean", - "default": false, - "description": "Search whole angular.io instead of just api." - } - }, - "required": [ + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/e2e-impl.ts b/packages/angular/cli/commands/e2e-impl.ts index 43d187fe561b..f24a44ca4122 100644 --- a/packages/angular/cli/commands/e2e-impl.ts +++ b/packages/angular/cli/commands/e2e-impl.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; +import { ArchitectCommand } from '../models/architect-command'; +import { Arguments } from '../models/interface'; +import { Schema as E2eCommandSchema } from './e2e'; -export class E2eCommand extends ArchitectCommand { +export class E2eCommand extends ArchitectCommand { public readonly target = 'e2e'; public readonly multiTarget = true; - public async run(options: ArchitectCommandOptions) { + public async run(options: E2eCommandSchema & Arguments) { return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/e2e-long.md b/packages/angular/cli/commands/e2e-long.md new file mode 100644 index 000000000000..6b651df713d9 --- /dev/null +++ b/packages/angular/cli/commands/e2e-long.md @@ -0,0 +1,2 @@ +Must be executed from within a workspace directory. +When a project name is not supplied, it will execute for all projects. \ No newline at end of file diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json index 8f659194cf7d..92c626d4c63e 100644 --- a/packages/angular/cli/commands/e2e.json +++ b/packages/angular/cli/commands/e2e.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/schema", - "id": "E2eCommandOptions", - "description": "", - "$longDescription": "", + "$id": "ng-cli://commands/e2e.json", + "description": "Builds and serves an Angular app, then runs end-to-end tests using Protractor.", + "$longDescription": "./e2e-long.md", "$aliases": [ "e" ], "$scope": "in", @@ -10,17 +10,8 @@ "$impl": "./e2e-impl#E2eCommand", "type": "object", - "properties": { - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts index c91fd44fde6b..8e70d46dc827 100644 --- a/packages/angular/cli/commands/easter-egg-impl.ts +++ b/packages/angular/cli/commands/easter-egg-impl.ts @@ -5,16 +5,16 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { terminal } from '@angular-devkit/core'; import { Command } 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 { - run() { +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!`, @@ -25,6 +25,6 @@ export class AwesomeCommand extends Command { `I spy with my little eye a great developer!`, `Noop... already awesome.`, ]); - this.logger.info(terminal.green(phrase)); + this.logger.info(colors.green(phrase)); } } diff --git a/packages/angular/cli/commands/easter-egg.json b/packages/angular/cli/commands/easter-egg.json index 18833da40982..d0a7e94189e9 100644 --- a/packages/angular/cli/commands/easter-egg.json +++ b/packages/angular/cli/commands/easter-egg.json @@ -1,11 +1,14 @@ { "$schema": "http://json-schema.org/schema", - "id": "EasterEggCommandOptions", + "$id": "ng-cli://commands/easter-egg.json", "description": "", "$longDescription": "", "$hidden": true, "$impl": "./easter-egg-impl#AwesomeCommand", - "type": "object" -} \ No newline at end of file + "type": "object", + "allOf": [ + { "$ref": "./definitions.json#/definitions/base" } + ] +} diff --git a/packages/angular/cli/commands/eject-impl.ts b/packages/angular/cli/commands/eject-impl.ts deleted file mode 100644 index cf755606eca2..000000000000 --- a/packages/angular/cli/commands/eject-impl.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { tags } from '@angular-devkit/core'; -import { Command, Option } from '../models/command'; - - -export class EjectCommand extends Command { - public readonly name = 'eject'; - public readonly description = 'Temporarily disabled. Ejects your app and output the proper ' - + 'webpack configuration and scripts.'; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - public static aliases = []; - - run() { - this.logger.info(tags.stripIndents` - The 'eject' command has been temporarily disabled, as it is not yet compatible with the new - angular.json format. The new configuration format provides further flexibility to modify the - configuration of your workspace without ejecting. Ejection will be re-enabled in a future - release of the CLI. - - If you need to eject today, use CLI 1.7 to eject your project. - `); - } -} diff --git a/packages/angular/cli/commands/eject.json b/packages/angular/cli/commands/eject.json deleted file mode 100644 index aa90f156c46b..000000000000 --- a/packages/angular/cli/commands/eject.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "id": "CommandOptions", - "description": "Temporarily disabled. Ejects your app and output the proper webpack configuration and scripts.", - "$longDescription": "", - - "$hidden": true, - "$scope": "in", - "$impl": "./eject-impl#EjectCommand", - - "type": "object" -} \ No newline at end of file diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 812e5fe2ed24..f384d30e87bc 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -5,65 +5,96 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any -import { tags, terminal } from '@angular-devkit/core'; +import { Arguments, SubCommandDescription } from '../models/interface'; import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; +import { colors } from '../utilities/color'; +import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema'; +import { Schema as GenerateCommandSchema } from './generate'; +export class GenerateCommand extends SchematicCommand { + // Allows us to resolve aliases before reporting analytics + longSchematicName: string | undefined; + + async initialize(options: GenerateCommandSchema & Arguments) { + // Fill up the schematics property of the command description. + const [collectionName, schematicName] = this.parseSchematicInfo(options); + this.collectionName = collectionName; + this.schematicName = schematicName; -export class GenerateCommand extends SchematicCommand { - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } await super.initialize(options); - this.initialized = true; - const [collectionName, schematicName] = this.parseSchematicInfo(options); - if (!!schematicName) { - const schematicOptions = await this.getOptions({ - schematicName, - collectionName, - }); - this.addOptions(schematicOptions); + const collection = this.getCollection(collectionName); + const subcommands: { [name: string]: SubCommandDescription } = {}; + + const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); + // Sort as a courtesy for the user. + schematicNames.sort(); + + for (const name of schematicNames) { + const schematic = this.getSchematic(collection, name, true); + this.longSchematicName = schematic.description.name; + let subcommand: SubCommandDescription; + if (schematic.description.schemaJson) { + subcommand = await parseJsonSchemaToSubCommandDescription( + name, + schematic.description.path, + this._workflow.registry, + schematic.description.schemaJson, + ); + } else { + continue; + } + + if (this.getDefaultSchematicCollection() == collectionName) { + subcommands[name] = subcommand; + } else { + subcommands[`${collectionName}:${name}`] = subcommand; + } } - } - validate(options: any): boolean | Promise { - if (!options.schematic) { - this.logger.error(tags.oneLine` - The "ng generate" command requires a - schematic name to be specified. - For more details, use "ng help".`); + this.description.options.forEach(option => { + if (option.name == 'schematic') { + option.subcommands = subcommands; + } + }); + } - return false; + public async run(options: GenerateCommandSchema & Arguments) { + if (!this.schematicName || !this.collectionName) { + return this.printHelp(options); } - return true; + return this.runSchematic({ + collectionName: this.collectionName, + schematicName: this.schematicName, + schematicOptions: options['--'] || [], + debug: !!options.debug || false, + dryRun: !!options.dryRun || false, + force: !!options.force || false, + }); } - public run(options: any) { + async reportAnalytics( + paths: string[], + options: GenerateCommandSchema & Arguments, + ): Promise { const [collectionName, schematicName] = this.parseSchematicInfo(options); - // remove the schematic name from the options - delete options.schematic; + if (!schematicName || !collectionName) { + return; + } + const escapedSchematicName = (this.longSchematicName || schematicName).replace(/\//g, '_'); - return this.runSchematic({ - collectionName, - schematicName, - schematicOptions: options, - debug: options.debug, - dryRun: options.dryRun, - force: options.force, - }); + return super.reportAnalytics( + ['generate', collectionName.replace(/\//g, '_'), escapedSchematicName], + options, + ); } - private parseSchematicInfo(options: any) { - let collectionName = getDefaultSchematicCollection(); + private parseSchematicInfo(options: { schematic?: string }): [string, string | undefined] { + let collectionName = this.getDefaultSchematicCollection(); - let schematicName: string = options.schematic; + let schematicName = options.schematic; if (schematicName) { if (schematicName.includes(':')) { @@ -74,26 +105,17 @@ export class GenerateCommand extends SchematicCommand { return [collectionName, schematicName]; } - public printHelp(_name: string, _description: string, options: any) { - const schematicName = options._[0]; - if (schematicName) { - const optsWithoutSchematic = this.options - .filter(o => !(o.name === 'schematic' && this.isArgument(o))); - this.printHelpUsage(`generate ${schematicName}`, optsWithoutSchematic); - this.printHelpOptions(this.options); - } else { - this.printHelpUsage('generate', this.options); - const engineHost = this.getEngineHost(); - const [collectionName] = this.parseSchematicInfo(options); - const collection = this.getCollection(collectionName); - const schematicNames: string[] = engineHost.listSchematicNames(collection.description); - this.logger.info('Available schematics:'); - schematicNames.forEach(schematicName => { - this.logger.info(` ${schematicName}`); - }); - - this.logger.warn(`\nTo see help for a schematic run:`); - this.logger.info(terminal.cyan(` ng generate --help`)); + public async printHelp(options: GenerateCommandSchema & Arguments) { + await super.printHelp(options); + + this.logger.info(''); + // Find the generate subcommand. + const subcommand = this.description.options.filter(x => x.subcommands)[0]; + if (Object.keys((subcommand && subcommand.subcommands) || {}).length == 1) { + this.logger.info(`\nTo see help for a schematic run:`); + this.logger.info(colors.cyan(` ng generate --help`)); } + + return 0; } } diff --git a/packages/angular/cli/commands/generate.json b/packages/angular/cli/commands/generate.json index 95c6c8a46e90..be287dd307c8 100644 --- a/packages/angular/cli/commands/generate.json +++ b/packages/angular/cli/commands/generate.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "GenerateCommandOptions", + "$id": "ng-cli://commands/generate.json", "description": "Generates and/or modifies files based on a schematic.", "$longDescription": "", @@ -9,29 +9,24 @@ "$type": "schematics", "$impl": "./generate-impl#GenerateCommand", - "type": "object", - "properties": { - "schematic": { - "type": "string", - "description": "The schematic or collection:schematic to generate.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "type": "object", + "properties": { + "schematic": { + "type": "string", + "description": "The schematic or collection:schematic to generate.", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": [ + ] }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Forces overwriting of files." - } - }, - "required": [ + { "$ref": "./definitions.json#/definitions/base" }, + { "$ref": "./definitions.json#/definitions/schematic" }, + { "$ref": "./definitions.json#/definitions/interactive" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/getset-impl.ts b/packages/angular/cli/commands/getset-impl.ts deleted file mode 100644 index 0e6c26795820..000000000000 --- a/packages/angular/cli/commands/getset-impl.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Command } from '../models/command'; - -export interface Options { - keyword: string; - search?: boolean; -} - -export class GetSetCommand extends Command { - public async run(_options: Options) { - this.logger.warn('get/set have been deprecated in favor of the config command.'); - } -} diff --git a/packages/angular/cli/commands/getset.json b/packages/angular/cli/commands/getset.json deleted file mode 100644 index 5d05f348a0ac..000000000000 --- a/packages/angular/cli/commands/getset.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "id": "GetSetCommandOptions", - "description": "Deprecated in favor of config command.", - "$longDescription": "", - - "$impl": "./getset-impl#GetSetCommand", - "$hidden": true, - "$type": "deprecated", - - "type": "object" -} \ No newline at end of file diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts index ac31379a27b8..9c30c5e2f608 100644 --- a/packages/angular/cli/commands/help-impl.ts +++ b/packages/angular/cli/commands/help-impl.ts @@ -5,36 +5,22 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any -import { terminal } from '@angular-devkit/core'; import { Command } from '../models/command'; +import { colors } from '../utilities/color'; +import { Schema as HelpCommandSchema } from './help'; -interface CommandInfo { - name: string; - description: string; - hidden: boolean; - aliases: string[]; -} - -export class HelpCommand extends Command { - run(options: any) { +export class HelpCommand extends Command { + async run() { this.logger.info(`Available Commands:`); - options.commandInfo - .filter((cmd: CommandInfo) => !cmd.hidden) - .forEach((cmd: CommandInfo) => { - let aliasInfo = ''; - if (cmd.aliases.length > 0) { - aliasInfo = ` (${cmd.aliases.join(', ')})`; - } - this.logger.info(` ${terminal.cyan(cmd.name)}${aliasInfo} ${cmd.description}`); - }); + 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"`); } - - printHelp(_commandName: string, _description: string, options: any) { - return this.run(options); - } } diff --git a/packages/angular/cli/commands/help-long.md b/packages/angular/cli/commands/help-long.md new file mode 100644 index 000000000000..b104a1a6c03e --- /dev/null +++ b/packages/angular/cli/commands/help-long.md @@ -0,0 +1,7 @@ + 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 index 6ce3882e17aa..b8df614b75b2 100644 --- a/packages/angular/cli/commands/help.json +++ b/packages/angular/cli/commands/help.json @@ -1,11 +1,15 @@ { "$schema": "http://json-schema.org/schema", - "id": "HelpCommandOptions", - "description": "Displays help for the Angular CLI.", - "$longDescription": "", + "$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" -} \ No newline at end of file + "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 index ad07ddfee80e..6edd8556f994 100644 --- a/packages/angular/cli/commands/lint-impl.ts +++ b/packages/angular/cli/commands/lint-impl.ts @@ -5,15 +5,15 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; +import { Arguments } from '../models/interface'; +import { Schema as LintCommandSchema } from './lint'; - -export class LintCommand extends ArchitectCommand { +export class LintCommand extends ArchitectCommand { public readonly target = 'lint'; public readonly multiTarget = true; - public async run(options: ArchitectCommandOptions) { + public async run(options: ArchitectCommandOptions & Arguments) { return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint-long.md new file mode 100644 index 000000000000..03917ffb252e --- /dev/null +++ b/packages/angular/cli/commands/lint-long.md @@ -0,0 +1,4 @@ +Takes the name of the project, as specified in the `projects` section of the `angular.json` workspace configuration file. +When a project name is not supplied, it will execute for all projects. + +The default linting tool is [TSLint](https://palantir.github.io/tslint/), and the default configuration is specified in the project's `tslint.json` file. \ No newline at end of file diff --git a/packages/angular/cli/commands/lint.json b/packages/angular/cli/commands/lint.json index 6340ca1e3d4a..eed1cda035c6 100644 --- a/packages/angular/cli/commands/lint.json +++ b/packages/angular/cli/commands/lint.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/schema", - "id": "LintCommandOptions", - "description": "Lints code in existing project.", - "$longDescription": "", + "$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", @@ -10,21 +10,30 @@ "$impl": "./lint-impl#LintCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to lint.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "project": { + "type": "string", + "description": "The name of the project to lint.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "The linting configuration to use.", + "type": "string", + "aliases": [ + "c" + ] + } + }, + "required": [ + ] }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index 0057fdcfbef5..1b1761680088 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -7,74 +7,43 @@ */ // tslint:disable:no-global-tslint-disable no-any +import { Arguments } from '../models/interface'; import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; +import { Schema as NewCommandSchema } from './new'; -export class NewCommand extends SchematicCommand { +export class NewCommand extends SchematicCommand { public readonly allowMissingWorkspace = true; - private schematicName = 'ng-new'; + schematicName = 'ng-new'; - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - - await super.initialize(options); - - this.initialized = true; - - const collectionName = this.parseCollectionName(options); - - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName, - }); - this.addOptions(this.options.concat(schematicOptions)); - } - - public async run(options: any) { - if (options.dryRun) { - options.skipGit = true; - } - - let collectionName: string; + async initialize(options: NewCommandSchema & Arguments) { if (options.collection) { - collectionName = options.collection; + this.collectionName = options.collection; } else { - collectionName = this.parseCollectionName(options); + this.collectionName = this.parseCollectionName(options); } - const packageJson = require('../package.json'); - options.version = packageJson.version; + return super.initialize(options); + } - // Ensure skipGit has a boolean value. - options.skipGit = options.skipGit === undefined ? false : options.skipGit; + public async run(options: NewCommandSchema & Arguments) { + // Register the version of the CLI in the registry. + const packageJson = require('../package.json'); + const version = packageJson.version; - options = this.removeLocalOptions(options); + this._workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version); return this.runSchematic({ - collectionName: collectionName, + collectionName: this.collectionName, schematicName: this.schematicName, - schematicOptions: options, - debug: options.debug, - dryRun: options.dryRun, - force: options.force, + schematicOptions: options['--'] || [], + debug: !!options.debug, + dryRun: !!options.dryRun, + force: !!options.force, }); } private parseCollectionName(options: any): string { - const collectionName = options.collection || options.c || getDefaultSchematicCollection(); - - return collectionName; - } - - private removeLocalOptions(options: any): any { - const opts = Object.assign({}, options); - delete opts.verbose; - delete opts.collection; - - return opts; + return options.collection || this.getDefaultSchematicCollection(); } } diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json index cf8104029909..89cfbda500dd 100644 --- a/packages/angular/cli/commands/new.json +++ b/packages/angular/cli/commands/new.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/schema", - "id": "NewCommandOptions", - "description": "Creates a new directory and a new Angular app.", - "$longDescription": "", + "$id": "ng-cli://commands/new.json", + "description": "Creates a new workspace and an initial Angular app.", + "$longDescription": "./new.md", "$aliases": [ "n" ], "$scope": "out", @@ -10,30 +10,25 @@ "$impl": "./new-impl#NewCommand", "type": "object", - "properties": { - "collection": { - "type": "string", - "aliases": ["c"], - "description": "Schematics collection to use." + "allOf": [ + { + "properties": { + "collection": { + "type": "string", + "aliases": [ "c" ], + "description": "A collection of schematics to use in generating the initial app." + }, + "verbose": { + "type": "boolean", + "default": false, + "aliases": [ "v" ], + "description": "When true, adds more details to output logging." + } + }, + "required": [] }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Forces overwriting of files." - }, - "verbose": { - "type": "boolean", - "default": false, - "aliases": ["v"], - "description": "Adds more details to output logging." - } - }, - "required": [] -} \ No newline at end of file + { "$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 new file mode 100644 index 000000000000..822deb745ee1 --- /dev/null +++ b/packages/angular/cli/commands/new.md @@ -0,0 +1,16 @@ +Creates and initializes a new Angular app that is the default project for a new workspace. + +Provides interactive prompts for optional configuration, such as adding routing support. +All prompts can safely be allowed to default. + +* The new workspace folder is given the specified project name, and contains configuration files at the top level. + +* By default, the files for a new initial app (with the same name as the workspace) are placed in the `src/` subfolder. Corresponding end-to-end tests are placed in the `e2e/` subfolder. + +* The new app's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name. + +* Subsequent apps that you generate in the workspace reside in the `projects/` subfolder. + +If you plan to have multiple apps in the workspace, you can create an empty workspace by setting the `--createApplication` option to false. +You can then use `ng generate application` to create an initial app. +This allows a workspace name different from the initial app name, and ensures that all apps reside in the `/projects` subfolder, matching the structure of the configuration file. \ No newline at end of file diff --git a/packages/angular/cli/commands/run-impl.ts b/packages/angular/cli/commands/run-impl.ts index ac98d3118486..feefe731fa5b 100644 --- a/packages/angular/cli/commands/run-impl.ts +++ b/packages/angular/cli/commands/run-impl.ts @@ -7,10 +7,11 @@ */ 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) { +export class RunCommand extends ArchitectCommand { + public async run(options: ArchitectCommandOptions & Arguments) { if (options.target) { return this.runArchitectTarget(options); } else { diff --git a/packages/angular/cli/commands/run-long.md b/packages/angular/cli/commands/run-long.md new file mode 100644 index 000000000000..a95bbd78a27a --- /dev/null +++ b/packages/angular/cli/commands/run-long.md @@ -0,0 +1,16 @@ +Architect is the tool that the CLI uses to perform complex tasks such as compilation, according to provided configurations. +The CLI commands run Architect targets such as `build`, `serve`, `test`, and `lint`. +Each named target has a default configuration, specified by an "options" object, +and an optional set of named alternate configurations in the "configurations" object. + +For example, the "serve" target for a newly generated app has a predefined +alternate configuration named "production". + +You can define new targets and their configuration options in the "architect" section +of the `angular.json` file. +If you do so, you can run them from the command line using the `ng run` command. +Execute the command using the following format. + +``` +ng run project:target[:configuration] +``` \ No newline at end of file diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json index c46b8b3e72e1..4111cc014a67 100644 --- a/packages/angular/cli/commands/run.json +++ b/packages/angular/cli/commands/run.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/schema", - "id": "RunCommandOptions", - "description": "Runs Architect targets.", - "$longDescription": "", + "$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", @@ -10,21 +10,28 @@ "$impl": "./run-impl#RunCommand", "type": "object", - "properties": { - "target": { - "type": "string", - "description": "The target to run.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "target": { + "type": "string", + "description": "The Architect target to run.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "A named builder configuration, defined in the \"configurations\" section of angular.json.\nThe builder uses the named configuration to run the given target.", + "type": "string", + "aliases": [ "c" ] + } + }, + "required": [ + ] }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts index 6ff9b11e62c3..dff1855e6e7a 100644 --- a/packages/angular/cli/commands/serve-impl.ts +++ b/packages/angular/cli/commands/serve-impl.ts @@ -5,23 +5,33 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - +import { analytics } from '@angular-devkit/core'; import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Version } from '../upgrade/version'; - +import { Arguments } from '../models/interface'; +import { Schema as BuildCommandSchema } from './build'; +import { Schema as ServeCommandSchema } from './serve'; -export class ServeCommand extends ArchitectCommand { +export class ServeCommand extends ArchitectCommand { public readonly target = 'serve'; - public validate(_options: ArchitectCommandOptions) { - // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); - + public validate(_options: ArchitectCommandOptions & Arguments) { return true; } - public async run(options: ArchitectCommandOptions) { + public async run(options: ArchitectCommandOptions & Arguments) { return this.runArchitectTarget(options); } + + async reportAnalytics( + paths: string[], + options: BuildCommandSchema & Arguments, + dimensions: (boolean | number | string)[] = [], + metrics: (boolean | number | string)[] = [], + ): Promise { + if (options.buildEventLog !== undefined) { + dimensions[analytics.NgCliAnalyticsDimensions.NgBuildBuildEventLog] = true; + } + + return super.reportAnalytics(paths, options, dimensions, metrics); + } } diff --git a/packages/angular/cli/commands/serve.json b/packages/angular/cli/commands/serve.json index d10ebccee6a9..e19f0e5bab56 100644 --- a/packages/angular/cli/commands/serve.json +++ b/packages/angular/cli/commands/serve.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "ServeCommandOptions", + "$id": "ng-cli://commands/serve.json", "description": "Builds and serves your app, rebuilding on file changes.", "$longDescription": "", @@ -10,25 +10,17 @@ "$impl": "./serve-impl#ServeCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to serve.", - "$default": { - "$source": "argv", - "index": 0 + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" }, + { + "type": "object", + "properties": { + "buildEventLog": { + "type": "string", + "description": "**EXPERIMENTAL** Output file path for Build Event Protocol events" + } } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts index 2692e811d3bb..28f09df4d7b2 100644 --- a/packages/angular/cli/commands/test-impl.ts +++ b/packages/angular/cli/commands/test-impl.ts @@ -7,12 +7,14 @@ */ import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; +import { Arguments } from '../models/interface'; +import { Schema as TestCommandSchema } from './test'; -export class TestCommand extends ArchitectCommand { +export class TestCommand extends ArchitectCommand { public readonly target = 'test'; public readonly multiTarget = true; - public async run(options: ArchitectCommandOptions) { + public async run(options: ArchitectCommandOptions & Arguments) { return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/test-long.md b/packages/angular/cli/commands/test-long.md new file mode 100644 index 000000000000..64dae312ab47 --- /dev/null +++ b/packages/angular/cli/commands/test-long.md @@ -0,0 +1,2 @@ +Takes the name of the project, as specified in the `projects` section of the `angular.json` workspace configuration file. +When a project name is not supplied, it will execute for all projects. \ No newline at end of file diff --git a/packages/angular/cli/commands/test.json b/packages/angular/cli/commands/test.json index f987e7fc5cbd..3b330a9cb71e 100644 --- a/packages/angular/cli/commands/test.json +++ b/packages/angular/cli/commands/test.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/schema", - "id": "TestCommandOptions", - "description": "Run unit tests in existing project.", - "$longDescription": "", + "$id": "ng-cli://commands/test.json", + "description": "Runs unit tests in a project.", + "$longDescription": "./test-long.md", "$aliases": [ "t" ], "$scope": "in", @@ -10,25 +10,8 @@ "$impl": "./test-impl#TestCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to test.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts index 0594d3c54921..3323a054c5f9 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/commands/update-impl.ts @@ -5,80 +5,370 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import { Arguments, Option } from '../models/interface'; +import { SchematicCommand } from '../models/schematic-command'; +import { getPackageManager } from '../utilities/package-manager'; +import { + PackageIdentifier, + PackageManifest, + fetchPackageMetadata, +} from '../utilities/package-metadata'; +import { + PackageTreeActual, + findNodeDependencies, + readPackageTree, +} from '../utilities/package-tree'; +import { Schema as UpdateCommandSchema } from './update'; -// tslint:disable:no-global-tslint-disable no-any -import { normalize } from '@angular-devkit/core'; -import { CommandScope, Option } from '../models/command'; -import { CoreSchematicOptions, SchematicCommand } from '../models/schematic-command'; -import { findUp } from '../utilities/find-up'; - -export interface UpdateOptions extends CoreSchematicOptions { - next: boolean; - schematic?: boolean; -} +const npa = require('npm-package-arg'); +const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json']; -export class UpdateCommand extends SchematicCommand { - public readonly name = 'update'; - public readonly description = 'Updates your application and its dependencies.'; - public static aliases: string[] = []; - public static scope = CommandScope.everywhere; - public arguments: string[] = [ 'packages' ]; - public options: Option[] = [ - // Remove the --force flag. - ...this.coreOptions.filter(option => option.name !== 'force'), - ]; +export class UpdateCommand extends SchematicCommand { public readonly allowMissingWorkspace = true; - private collectionName = '@schematics/update'; - private schematicName = 'update'; + async parseArguments(_schematicOptions: string[], _schema: Option[]): Promise { + return {}; + } + + // tslint:disable-next-line:no-big-function + async run(options: UpdateCommandSchema & Arguments) { + const packages: PackageIdentifier[] = []; + for (const request of options['--'] || []) { + try { + const packageIdentifier: PackageIdentifier = npa(request); + + // only registry identifiers are supported + if (!packageIdentifier.registry) { + this.logger.error(`Package '${request}' is not a registry package identifer.`); + + return 1; + } - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; + if (packages.some(v => v.name === packageIdentifier.name)) { + this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`); + + return 1; + } + + // If next option is used and no specifier supplied, use next tag + if (options.next && !packageIdentifier.rawSpec) { + packageIdentifier.fetchSpec = 'next'; + } + + packages.push(packageIdentifier); + } catch (e) { + this.logger.error(e.message); + + return 1; + } } - await super.initialize(options); - this.initialized = true; - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName: this.collectionName, - }); - this.addOptions(schematicOptions); - } + if (options.all && packages.length > 0) { + this.logger.error('Cannot specify packages when using the "all" option.'); + + return 1; + } else if (options.all && options.migrateOnly) { + this.logger.error('Cannot use "all" option with "migrate-only" option.'); + + return 1; + } else if (!options.migrateOnly && (options.from || options.to)) { + this.logger.error('Can only use "from" or "to" options with "migrate-only" option.'); + + return 1; + } + + // If not asking for status then check for a clean git repository. + // This allows the user to easily reset any changes from the update. + const statusCheck = packages.length === 0 && !options.all; + if (!statusCheck && !this.checkCleanGit()) { + if (options.allowDirty) { + this.logger.warn( + 'Repository is not clean. Update changes will be mixed with pre-existing changes.', + ); + } else { + this.logger.error( + 'Repository is not clean. Please commit or stash any changes before updating.', + ); - async validate(options: any) { - if (options._[0] == '@angular/cli' - && options.migrateOnly === undefined - && options.from === undefined) { - // Check for a 1.7 angular-cli.json file. - const oldConfigFileNames = [ - normalize('.angular-cli.json'), - normalize('angular-cli.json'), - ]; - const oldConfigFilePath = - findUp(oldConfigFileNames, process.cwd()) - || findUp(oldConfigFileNames, __dirname); - - if (oldConfigFilePath) { - options.migrateOnly = true; - options.from = '1.0.0'; + return 2; } } - return super.validate(options); - } + const packageManager = getPackageManager(this.workspace.root); + this.logger.info(`Using package manager: '${packageManager}'`); + + // Special handling for Angular CLI 1.x migrations + if ( + options.migrateOnly === undefined && + options.from === undefined && + !options.all && + packages.length === 1 && + packages[0].name === '@angular/cli' && + this.workspace.configFile && + oldConfigFileNames.includes(this.workspace.configFile) + ) { + options.migrateOnly = true; + options.from = '1.0.0'; + } + + this.logger.info('Collecting installed dependencies...'); + + const packageTree = await readPackageTree(this.workspace.root); + const rootDependencies = findNodeDependencies(packageTree); + + this.logger.info(`Found ${Object.keys(rootDependencies).length} dependencies.`); + + if (options.all || packages.length === 0) { + // Either update all packages or show status + return this.runSchematic({ + collectionName: '@schematics/update', + schematicName: 'update', + dryRun: !!options.dryRun, + showNothingDone: false, + additionalOptions: { + force: options.force || false, + next: options.next || false, + verbose: options.verbose || false, + packageManager, + packages: options.all ? Object.keys(rootDependencies) : [], + }, + }); + } + + if (options.migrateOnly) { + if (!options.from) { + this.logger.error('"from" option is required when using the "migrate-only" option.'); + + return 1; + } else if (packages.length !== 1) { + this.logger.error( + 'A single package must be specified when using the "migrate-only" option.', + ); + + return 1; + } + + if (options.next) { + this.logger.warn('"next" option has no effect when using "migrate-only" option.'); + } + + const packageName = packages[0].name; + let packageNode = rootDependencies[packageName]; + if (typeof packageNode === 'string') { + this.logger.error('Package found in package.json but is not installed.'); + + return 1; + } else if (!packageNode) { + // Allow running migrations on transitively installed dependencies + // There can technically be nested multiple versions + // TODO: If multiple, this should find all versions and ask which one to use + const child = packageTree.children.find(c => c.name === packageName); + if (child) { + // A link represents a symlinked package so use the actual in this case + packageNode = child.isLink ? child.target : child; + } + + if (!packageNode) { + this.logger.error('Package is not installed.'); + + return 1; + } + } + + const updateMetadata = packageNode.package['ng-update']; + let migrations = updateMetadata && updateMetadata.migrations; + if (migrations === undefined) { + this.logger.error('Package does not provide migrations.'); + + return 1; + } else if (typeof migrations !== 'string') { + this.logger.error('Package contains a malformed migrations field.'); + + return 1; + } else if (path.posix.isAbsolute(migrations) || path.win32.isAbsolute(migrations)) { + this.logger.error( + 'Package contains an invalid migrations field. Absolute paths are not permitted.', + ); + + return 1; + } + + // Normalize slashes + migrations = migrations.replace(/\\/g, '/'); + if (migrations.startsWith('../')) { + this.logger.error( + 'Package contains an invalid migrations field. ' + + 'Paths outside the package root are not permitted.', + ); + + return 1; + } + + // Check if it is a package-local location + const localMigrations = path.join(packageNode.path, migrations); + if (fs.existsSync(localMigrations)) { + migrations = localMigrations; + } else { + // Try to resolve from package location. + // This avoids issues with package hoisting. + try { + migrations = require.resolve(migrations, { paths: [packageNode.path] }); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + this.logger.error('Migrations for package were not found.'); + } else { + this.logger.error(`Unable to resolve migrations for package. [${e.message}]`); + } + + return 1; + } + } + + return this.runSchematic({ + collectionName: '@schematics/update', + schematicName: 'migrate', + dryRun: !!options.dryRun, + force: false, + showNothingDone: false, + additionalOptions: { + package: packageName, + collection: migrations, + from: options.from, + verbose: options.verbose || false, + to: options.to || packageNode.package.version, + }, + }); + } + + const requests: { + identifier: PackageIdentifier; + node: PackageTreeActual | string; + }[] = []; + + // Validate packages actually are part of the workspace + for (const pkg of packages) { + const node = rootDependencies[pkg.name]; + if (!node) { + this.logger.error(`Package '${pkg.name}' is not a dependency.`); + + return 1; + } + + // If a specific version is requested and matches the installed version, skip. + if ( + pkg.type === 'version' && + typeof node === 'object' && + node.package.version === pkg.fetchSpec + ) { + this.logger.info(`Package '${pkg.name}' is already at '${pkg.fetchSpec}'.`); + continue; + } + + requests.push({ identifier: pkg, node }); + } + + if (requests.length === 0) { + return 0; + } + + const packagesToUpdate: string[] = []; + + this.logger.info('Fetching dependency metadata from registry...'); + for (const { identifier: requestIdentifier, node } of requests) { + const packageName = requestIdentifier.name; + + let metadata; + try { + // Metadata requests are internally cached; multiple requests for same name + // does not result in additional network traffic + metadata = await fetchPackageMetadata(packageName, this.logger, { 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') { + manifest = metadata.versions.get(requestIdentifier.fetchSpec); + } else if (requestIdentifier.type === 'range') { + const maxVersion = semver.maxSatisfying( + Array.from(metadata.versions.keys()), + requestIdentifier.fetchSpec, + ); + if (maxVersion) { + manifest = metadata.versions.get(maxVersion); + } + } else if (requestIdentifier.type === 'tag') { + manifest = metadata.tags[requestIdentifier.fetchSpec]; + } + + if (!manifest) { + this.logger.error( + `Package specified by '${requestIdentifier.raw}' does not exist within the registry.`, + ); + + return 1; + } + + if ( + (typeof node === 'string' && manifest.version === node) || + (typeof node === 'object' && manifest.version === node.package.version) + ) { + this.logger.info(`Package '${packageName}' is already up to date.`); + continue; + } + + packagesToUpdate.push(requestIdentifier.toString()); + } + + if (packagesToUpdate.length === 0) { + return 0; + } - public async run(options: UpdateOptions) { return this.runSchematic({ - collectionName: this.collectionName, - schematicName: this.schematicName, - schematicOptions: options, - dryRun: options.dryRun, - force: false, + collectionName: '@schematics/update', + schematicName: 'update', + dryRun: !!options.dryRun, showNothingDone: false, + additionalOptions: { + verbose: options.verbose || false, + force: options.force || false, + packageManager, + packages: packagesToUpdate, + }, }); } + + checkCleanGit() { + try { + const result = execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' }); + if (result.trim().length === 0) { + return true; + } + + // Only files inside the workspace root are relevant + for (const entry of result.split('\n')) { + const relativeEntry = path.relative( + path.resolve(this.workspace.root), + path.resolve(entry.slice(3).trim()), + ); + + if (!relativeEntry.startsWith('..') && !path.isAbsolute(relativeEntry)) { + return false; + } + } + + } catch { } + + return true; + } } diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update-long.md new file mode 100644 index 000000000000..2132f0ed69dd --- /dev/null +++ b/packages/angular/cli/commands/update-long.md @@ -0,0 +1,9 @@ +Perform a basic update to the current stable release of the core framework and CLI by running the following command. + +``` +ng update @angular/cli @angular/core +``` + +To update to the next beta or pre-release version, use the `--next=true` option. + +For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.io/). diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json index dd66520136e0..41e19d672f2b 100644 --- a/packages/angular/cli/commands/update.json +++ b/packages/angular/cli/commands/update.json @@ -1,29 +1,69 @@ { "$schema": "http://json-schema.org/schema", - "id": "UpdateCommandOptions", - "description": "Updates your application and its dependencies.", - "$longDescription": "", + "$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", - "properties": { - "packages": { - "type": "string", - "description": "The names of package(s) to update", - "$default": { - "$source": "argv" - } + "allOf": [ + { + "$ref": "./definitions.json#/definitions/base" }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." + { + "type": "object", + "properties": { + "packages": { + "description": "The names of package(s) to update.", + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "argv" + } + }, + "force": { + "description": "If false, will error out if installed packages are incompatible with the update.", + "default": false, + "type": "boolean" + }, + "all": { + "description": "Whether to update all packages in package.json.", + "default": false, + "type": "boolean" + }, + "next": { + "description": "Use the largest version, including beta and RCs.", + "default": false, + "type": "boolean" + }, + "migrateOnly": { + "description": "Only perform a migration, does not update the installed version.", + "type": "boolean" + }, + "from": { + "description": "Version from which to migrate from. Only available with a single package being updated, and only on migration only.", + "type": "string" + }, + "to": { + "description": "Version up to which to apply migrations. Only available with a single package being updated, and only on migrations only. Requires from to be specified. Default to the installed version detected.", + "type": "string" + }, + "allowDirty": { + "description": "Whether to allow updating when the repository contains modified or untracked files.", + "type": "boolean" + }, + "verbose": { + "description": "Display additional details about internal operations during execution.", + "type": "boolean", + "default": false + } + } } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts index 2b9b740d6d58..acf6d0d5bdb0 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version-impl.ts @@ -5,23 +5,22 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { terminal } from '@angular-devkit/core'; import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { Command } from '../models/command'; +import { colors } from '../utilities/color'; import { findUp } from '../utilities/find-up'; +import { Schema as VersionCommandSchema } from './version'; - -export class VersionCommand extends Command { +export class VersionCommand extends Command { public static aliases = ['v']; - public run() { + async run() { const pkg = require(path.resolve(__dirname, '..', 'package.json')); let projPkg; try { - projPkg = require(path.resolve(this.project.root, 'package.json')); + projPkg = require(path.resolve(this.workspace.root, 'package.json')); } catch (exception) { projPkg = undefined; } @@ -29,7 +28,9 @@ export class VersionCommand extends Command { const patterns = [ /^@angular\/.*/, /^@angular-devkit\/.*/, + /^@bazel\/.*/, /^@ngtools\/.*/, + /^@nguniversal\/.*/, /^@schematics\/.*/, /^rxjs$/, /^typescript$/, @@ -39,53 +40,57 @@ export class VersionCommand extends Command { const maybeNodeModules = findUp('node_modules', __dirname); const packageRoot = projPkg - ? path.resolve(this.project.root, 'node_modules') + ? path.resolve(this.workspace.root, 'node_modules') : maybeNodeModules; const packageNames = [ - ...Object.keys(pkg && pkg['dependencies'] || {}), - ...Object.keys(pkg && pkg['devDependencies'] || {}), - ...Object.keys(projPkg && projPkg['dependencies'] || {}), - ...Object.keys(projPkg && projPkg['devDependencies'] || {}), - ]; + ...Object.keys((pkg && pkg['dependencies']) || {}), + ...Object.keys((pkg && pkg['devDependencies']) || {}), + ...Object.keys((projPkg && projPkg['dependencies']) || {}), + ...Object.keys((projPkg && projPkg['devDependencies']) || {}), + ]; if (packageRoot != null) { // Add all node_modules and node_modules/@*/* - const nodePackageNames = fs.readdirSync(packageRoot) - .reduce((acc, name) => { - if (name.startsWith('@')) { - return acc.concat( - fs.readdirSync(path.resolve(packageRoot, name)) - .map(subName => name + '/' + subName), - ); - } else { - return acc.concat(name); - } - }, []); + const nodePackageNames = fs.readdirSync(packageRoot).reduce((acc, name) => { + if (name.startsWith('@')) { + return acc.concat( + fs.readdirSync(path.resolve(packageRoot, name)).map(subName => name + '/' + subName), + ); + } else { + return acc.concat(name); + } + }, []); packageNames.push(...nodePackageNames); } const versions = packageNames .filter(x => patterns.some(p => p.test(x))) - .reduce((acc, name) => { - if (name in acc) { - return acc; - } + .reduce( + (acc, name) => { + if (name in acc) { + return acc; + } - acc[name] = this.getVersion(name, packageRoot, maybeNodeModules); + acc[name] = this.getVersion(name, packageRoot, maybeNodeModules); - return acc; - }, {} as { [module: string]: string }); + return acc; + }, + {} as { [module: string]: string }, + ); let ngCliVersion = pkg.version; if (!__dirname.match(/node_modules/)) { let gitBranch = '??'; try { - const gitRefName = '' + child_process.execSync('git symbolic-ref HEAD', {cwd: __dirname}); - gitBranch = path.basename(gitRefName.replace('\n', '')); - } catch { - } + const gitRefName = child_process.execSync('git rev-parse --abbrev-ref HEAD', { + cwd: __dirname, + encoding: 'utf8', + stdio: 'pipe', + }); + gitBranch = gitRefName.replace('\n', ''); + } catch {} ngCliVersion = `local (v${pkg.version}, branch: ${gitBranch})`; } @@ -97,8 +102,10 @@ export class VersionCommand extends Command { angularCoreVersion = versions['@angular/core']; if (angularCoreVersion) { for (const angularPackage of Object.keys(versions)) { - if (versions[angularPackage] == angularCoreVersion - && angularPackage.startsWith('@angular/')) { + if ( + versions[angularPackage] == angularCoreVersion && + angularPackage.startsWith('@angular/') + ) { angularSameAsCore.push(angularPackage.replace(/^@angular\//, '')); delete versions[angularPackage]; } @@ -119,36 +126,43 @@ export class VersionCommand extends Command { / ___ \\| | | | (_| | |_| | | (_| | | | |___| |___ | | /_/ \\_\\_| |_|\\__, |\\__,_|_|\\__,_|_| \\____|_____|___| |___/ - `.split('\n').map(x => terminal.red(x)).join('\n'); + ` + .split('\n') + .map(x => colors.red(x)) + .join('\n'); this.logger.info(asciiArt); - this.logger.info(` + this.logger.info( + ` Angular CLI: ${ngCliVersion} Node: ${process.versions.node} OS: ${process.platform} ${process.arch} Angular: ${angularCoreVersion} - ... ${angularSameAsCore.reduce((acc, name) => { - // Perform a simple word wrap around 60. - if (acc.length == 0) { - return [name]; - } - const line = (acc[acc.length - 1] + ', ' + name); - if (line.length > 60) { - acc.push(name); - } else { - acc[acc.length - 1] = line; - } + ... ${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... ')} + 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, '')); + .map(module => `${module}${namePad.slice(module.length)}${versions[module]}`) + .sort() + .join('\n')} + `.replace(/^ {6}/gm, ''), + ); } private getVersion( @@ -162,8 +176,7 @@ export class VersionCommand extends Command { return modulePkg.version; } - } catch (_) { - } + } catch (_) {} try { if (cliNodeModules) { @@ -171,8 +184,7 @@ export class VersionCommand extends Command { return modulePkg.version + ' (cli-only)'; } - } catch { - } + } catch {} return ''; } diff --git a/packages/angular/cli/commands/version.json b/packages/angular/cli/commands/version.json index 8d8fda9f1c96..795eb654b7a5 100644 --- a/packages/angular/cli/commands/version.json +++ b/packages/angular/cli/commands/version.json @@ -1,11 +1,15 @@ { "$schema": "http://json-schema.org/schema", - "id": "VersionCommandOptions", + "$id": "ng-cli://commands/version.json", "description": "Outputs Angular CLI version.", "$longDescription": "", "$aliases": [ "v" ], + "$scope": "all", "$impl": "./version-impl#VersionCommand", - "type": "object" -} \ No newline at end of file + "type": "object", + "allOf": [ + { "$ref": "./definitions.json#/definitions/base" } + ] +} diff --git a/packages/angular/cli/commands/xi18n-impl.ts b/packages/angular/cli/commands/xi18n-impl.ts index 39c5c3368fcd..5412750138df 100644 --- a/packages/angular/cli/commands/xi18n-impl.ts +++ b/packages/angular/cli/commands/xi18n-impl.ts @@ -6,14 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; +import { ArchitectCommand } from '../models/architect-command'; +import { Arguments } from '../models/interface'; +import { Schema as Xi18nCommandSchema } from './xi18n'; - -export class Xi18nCommand extends ArchitectCommand { +export class Xi18nCommand extends ArchitectCommand { public readonly target = 'extract-i18n'; public readonly multiTarget: true; - public async run(options: ArchitectCommandOptions) { + public async run(options: Xi18nCommandSchema & Arguments) { + const version = process.version.substr(1).split('.'); + if (Number(version[0]) === 12 && Number(version[1]) === 0) { + this.logger.error( + 'Due to a defect in Node.js 12.0, the command is not supported on this Node.js version. ' + + 'Please upgrade to Node.js 12.1 or later.'); + + return 1; + } + return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/xi18n.json b/packages/angular/cli/commands/xi18n.json index 0c55c95e5a1c..082ae7ca1ba2 100644 --- a/packages/angular/cli/commands/xi18n.json +++ b/packages/angular/cli/commands/xi18n.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "Xi18nCommandOptions", + "$id": "ng-cli://commands/xi18n.json", "description": "Extracts i18n messages from source code.", "$longDescription": "", @@ -10,21 +10,8 @@ "$impl": "./xi18n-impl#Xi18nCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to extract.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/custom-typings.d.ts b/packages/angular/cli/custom-typings.d.ts deleted file mode 100644 index b33ea8011637..000000000000 --- a/packages/angular/cli/custom-typings.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -// tslint:disable:no-global-tslint-disable no-any -declare module 'yargs-parser' { - const parseOptions: any; - const yargsParser: (args: string | string[], options?: typeof parseOptions) => T; - export = yargsParser; -} - -declare module 'json-schema-traverse' { - import { JsonObject } from '@angular-devkit/core'; - interface TraverseOptions { - allKeys?: boolean; - } - type TraverseCallback = ( - schema: JsonObject, - jsonPointer: string, - rootSchema: string, - parentJsonPointer: string, - parentKeyword: string, - parentSchema: string, - property: string) => void; - - interface TraverseCallbacks { - pre?: TraverseCallback; - post?: TraverseCallback; - } - - const traverse: (schema: object, options: TraverseOptions, cbs: TraverseCallbacks) => void; - - export = traverse; -} diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index 6be758d2b05e..370040cb90d4 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -5,32 +5,55 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { logging, terminal } from '@angular-devkit/core'; -import { filter } from 'rxjs/operators'; +import { createConsoleLogger } from '@angular-devkit/core/node'; +import { normalize } from 'path'; +import { format } from 'util'; import { runCommand } from '../../models/command-runner'; -import { getProjectDetails } from '../../utilities/project'; - +import { colors, supportsColor } from '../../utilities/color'; +import { getWorkspaceRaw } from '../../utilities/config'; +import { getWorkspaceDetails } from '../../utilities/project'; -export default async function(options: { testing?: boolean, cliArgs: string[] }) { - // const commands = await loadCommands(); +// tslint:disable: no-console +export default async function(options: { testing?: boolean; cliArgs: string[] }) { + const logger = createConsoleLogger(false, process.stdout, process.stderr, { + info: s => (supportsColor ? s : colors.unstyle(s)), + debug: s => (supportsColor ? s : colors.unstyle(s)), + warn: s => (supportsColor ? colors.bold.yellow(s) : colors.unstyle(s)), + error: s => (supportsColor ? colors.bold.red(s) : colors.unstyle(s)), + fatal: s => (supportsColor ? colors.bold.red(s) : colors.unstyle(s)), + }); - const logger = new logging.IndentLogger('cling'); - let loggingSubscription; - if (!options.testing) { - loggingSubscription = initializeLogging(logger); - } + // Redirect console to logger + console.log = function() { + logger.info(format.apply(null, arguments)); + }; + console.info = function() { + logger.info(format.apply(null, arguments)); + }; + console.warn = function() { + logger.warn(format.apply(null, arguments)); + }; + console.error = function() { + logger.error(format.apply(null, arguments)); + }; - let projectDetails = getProjectDetails(); + let projectDetails = getWorkspaceDetails(); if (projectDetails === null) { + const [, localPath] = getWorkspaceRaw('local'); + if (localPath !== null) { + logger.fatal( + `An invalid configuration file was found ['${localPath}'].` + + ' Please delete the file before running the command.', + ); + + return 1; + } + projectDetails = { root: process.cwd() }; } - const context = { - project: projectDetails, - }; try { - const maybeExitCode = await runCommand(options.cliArgs, logger, context); + const maybeExitCode = await runCommand(options.cliArgs, logger, projectDetails); if (typeof maybeExitCode === 'number') { console.assert(Number.isInteger(maybeExitCode)); @@ -40,10 +63,28 @@ export default async function(options: { testing?: boolean, cliArgs: string[] }) return 0; } catch (err) { if (err instanceof Error) { - logger.fatal(err.message); - if (err.stack) { - logger.fatal(err.stack); + try { + const fs = await import('fs'); + const os = await import('os'); + const tempDirectory = fs.mkdtempSync(fs.realpathSync(os.tmpdir()) + '/' + 'ng-'); + const logPath = normalize(tempDirectory + '/angular-errors.log'); + fs.appendFileSync(logPath, '[error] ' + (err.stack || err)); + + logger.fatal( + `An unhandled exception occurred: ${err.message}\n` + + `See "${logPath}" for further details.`, + ); + } catch (e) { + logger.fatal( + `An unhandled exception occurred: ${err.message}\n` + + `Fatal error writing debug log file: ${e.message}`, + ); + if (err.stack) { + logger.fatal(err.stack); + } } + + return 127; } else if (typeof err === 'string') { logger.fatal(err); } else if (typeof err === 'number') { @@ -53,42 +94,11 @@ export default async function(options: { testing?: boolean, cliArgs: string[] }) } if (options.testing) { + // tslint:disable-next-line: no-debugger debugger; throw err; } - if (loggingSubscription) { - loggingSubscription.unsubscribe(); - } - return 1; } } - -// Initialize logging. -function initializeLogging(logger: logging.Logger) { - return logger - .pipe(filter(entry => (entry.level != 'debug'))) - .subscribe(entry => { - let color = (x: string) => terminal.dim(terminal.white(x)); - let output = process.stdout; - switch (entry.level) { - case 'info': - color = terminal.white; - break; - case 'warn': - color = terminal.yellow; - break; - case 'error': - color = terminal.red; - output = process.stderr; - break; - case 'fatal': - color = (x) => terminal.bold(terminal.red(x)); - output = process.stderr; - break; - } - - output.write(color(entry.message) + '\n'); - }); -} diff --git a/packages/angular/cli/lib/config/.gitignore b/packages/angular/cli/lib/config/.gitignore deleted file mode 100644 index 879ebeae0a5f..000000000000 --- a/packages/angular/cli/lib/config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -schema.d.ts \ No newline at end of file diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index 4183c0aee9a8..1b7b03daa269 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -49,7 +49,7 @@ "packageManager": { "description": "Specify which package manager tool to use.", "type": "string", - "enum": [ "npm", "cnpm", "yarn" ] + "enum": [ "npm", "cnpm", "yarn", "pnpm" ] }, "warnings": { "description": "Control CLI specific console warnings", @@ -61,9 +61,14 @@ }, "typescriptMismatch": { "description": "Show a warning when the TypeScript version is incompatible.", - "type": "boolean" + "type": "boolean", + "x-deprecated": true } } + }, + "analytics": { + "type": "boolean", + "description": "Share anonymous usage data with the Angular Team at Google." } }, "additionalProperties": false @@ -93,7 +98,7 @@ }, "flat": { "type": "boolean", - "description": "Flag to indicate if a dir is created.", + "description": "Flag to indicate if a directory is created.", "default": false }, "inlineStyle": { @@ -108,7 +113,7 @@ "default": false, "alias": "t" }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -139,9 +144,21 @@ "type": "string", "default": "css" }, + "style": { + "description": "The file extension or preprocessor to use for style files.", + "type": "string", + "default": "css", + "enum": [ + "css", + "scss", + "sass", + "less", + "styl" + ] + }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", - "enum": ["Emulated", "Native", "None"], + "enum": ["Emulated", "Native", "None", "ShadowDom"], "type": "string", "alias": "v" } @@ -157,10 +174,10 @@ }, "flat": { "type": "boolean", - "description": "Flag to indicate if a dir is created.", + "description": "Flag to indicate if a directory is created.", "default": true }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -186,6 +203,11 @@ "type": "boolean", "description": "Specifies if a spec file is generated.", "default": true + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create test files.", + "default": false } } }, @@ -203,14 +225,9 @@ "description": "The scope for the generated routing.", "default": "Child" }, - "spec": { - "type": "boolean", - "description": "Specifies if a spec file is generated.", - "default": true - }, "flat": { "type": "boolean", - "description": "Flag to indicate if a dir is created.", + "description": "Flag to indicate if a directory is created.", "default": false }, "commonModule": { @@ -219,7 +236,7 @@ "default": true, "visible": false }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -232,12 +249,17 @@ "flat": { "type": "boolean", "default": true, - "description": "Flag to indicate if a dir is created." + "description": "Flag to indicate if a directory is created." }, "spec": { "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." + "description": "Specifies if a spec file is generated.", + "default": true + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create test files.", + "default": false } } }, @@ -247,12 +269,17 @@ "flat": { "type": "boolean", "default": true, - "description": "Flag to indicate if a dir is created." + "description": "Flag to indicate if a directory is created." }, "spec": { "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." + "description": "Specifies if a spec file is generated.", + "default": true + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create test files.", + "default": false }, "skipImport": { "type": "boolean", @@ -277,8 +304,13 @@ "properties": { "spec": { "type": "boolean", - "default": true, - "description": "Specifies if a spec file is generated." + "description": "Specifies if a spec file is generated.", + "default": true + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create test files.", + "default": false } } } @@ -327,12 +359,40 @@ "additionalProperties": { "$ref": "#/definitions/project/definitions/target" } + }, + "targets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/project/definitions/target" + } } }, "required": [ "root", "projectType" ], + "anyOf": [ + { + "required": ["architect"], + "not": { + "required": ["targets"] + } + }, + { + "required": ["targets"], + "not": { + "required": ["architect"] + } + }, + { + "not": { + "required": [ + "targets", + "architect" + ] + } + } + ], "additionalProperties": false, "patternProperties": { "^[a-z]{1,3}-.*": {} @@ -492,7 +552,7 @@ "type": "null", "definitions": { "appShell": { - "description": "App Shell target options for Build Facade.", + "description": "App Shell target options for Architect.", "type": "object", "properties": { "browserTarget": { @@ -524,7 +584,7 @@ "additionalProperties": false }, "browser": { - "title": "Webpack browser schema for Build Facade.", + "title": "Webpack browser schema for Architect.", "description": "Browser target options", "properties": { "assets": { @@ -579,9 +639,28 @@ "additionalProperties": false }, "optimization": { - "type": "boolean", "description": "Enables optimization of the build output.", - "default": false + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Enables optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fileReplacements": { "description": "Replace files with other files in the build.", @@ -595,15 +674,49 @@ "type": "string", "description": "Path where output will be placed." }, + "resourcesOutputPath": { + "type": "string", + "description": "The path where style resources will be placed, relative to outputPath." + }, "aot": { "type": "boolean", "description": "Build using Ahead of Time compilation.", "default": false }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", @@ -724,6 +837,10 @@ "description": "Generates a service worker config for production builds.", "default": false }, + "ngswConfigPath": { + "type": "string", + "description": "Path to ngsw-config.json." + }, "skipAppShell": { "type": "boolean", "description": "Flag to prevent building an app shell.", @@ -735,7 +852,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse .", + "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, "forkTypeChecker": { @@ -758,6 +875,31 @@ "$ref": "#/definitions/targetOptions/definitions/browser/definitions/budget" }, "default": [] + }, + "es5BrowserSupport": { + "description": "Enables conditionally loaded ES2015 polyfills.", + "type": "boolean", + "default": false + }, + "rebaseRootRelativeCssUrls": { + "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", + "type": "boolean", + "default": false, + "x-deprecated": true + }, + "webWorkerTsConfig": { + "type": "string", + "description": "TypeScript configuration for Web Worker modules." + }, + "crossOrigin": { + "type": "string", + "description": "Define the crossorigin attribute setting of elements that provide CORS support.", + "default": "none", + "enum": [ + "none", + "anonymous", + "use-credentials" + ] } }, "additionalProperties": false, @@ -778,6 +920,13 @@ "output": { "type": "string", "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -846,6 +995,11 @@ "type": "boolean", "description": "If the bundle will be lazy loaded.", "default": false + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true } }, "additionalProperties": false, @@ -870,6 +1024,7 @@ "allScript", "any", "anyScript", + "anyComponentStyle", "bundle", "initial" ] @@ -915,7 +1070,7 @@ } }, "devServer": { - "description": "Dev Server target options for Build Facade.", + "description": "Dev Server target options for Architect.", "type": "object", "properties": { "browserTarget": { @@ -951,18 +1106,18 @@ }, "open": { "type": "boolean", - "description": "Opens the url in default browser.", + "description": "When true, open the live-reload URL in default browser.", "default": false, "alias": "o" }, "liveReload": { "type": "boolean", - "description": "Whether to reload the page on change, using live-reload.", + "description": "When true, reload the page on change using live-reload.", "default": true }, "publicHost": { "type": "string", - "description": "Specify the URL that the browser client will use." + "description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies." }, "servePath": { "type": "string", @@ -970,57 +1125,103 @@ }, "disableHostCheck": { "type": "boolean", - "description": "Don't verify connected clients are part of allowed hosts.", + "description": "When true, don't verify that connected clients are part of allowed hosts.", "default": false }, "hmr": { "type": "boolean", - "description": "Enable hot module replacement.", + "description": "When true, enable hot module replacement.", "default": false }, "watch": { "type": "boolean", - "description": "Rebuild on change.", + "description": "When true, rebuild on change.", "default": true }, "hmrWarning": { "type": "boolean", - "description": "Show a warning when the --hmr option is enabled.", + "description": "When true, show a warning when the --hmr option is enabled.", "default": true }, "servePathDefaultWarning": { "type": "boolean", - "description": "Show a warning when deploy-url/base-href use unsupported serve path values.", + "description": "When true, show a warning when deploy-url/base-href use unsupported serve path values.", "default": true }, "optimization": { - "type": "boolean", - "description": "Enables optimization of the build output." + "description": "Enable optimization of the build output.", + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "When true, enable optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "When true, enable optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "aot": { "type": "boolean", - "description": "Build using Ahead of Time compilation." + "description": "Build using ahead-of-time compilation." }, "sourceMap": { - "type": "boolean", - "description": "Output sourcemaps." + "description": "When true, output sourcemaps.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "When true, output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "When true, output sourcemaps for all styles.", + "default": true + }, + "vendor": { + "type": "boolean", + "description": "When true, resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "When true, resolve vendor packages sourcemaps.", "default": false }, "evalSourceMap": { "type": "boolean", - "description": "Output in-file eval sourcemaps." + "description": "When true, output in-file eval sourcemaps." }, "vendorChunk": { "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries." + "description": "When true, use a separate bundle containing only vendor libraries." }, "commonChunk": { "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles." + "description": "When true, use a separate bundle containing code used across multiple bundles." }, "baseHref": { "type": "string", @@ -1032,17 +1233,17 @@ }, "verbose": { "type": "boolean", - "description": "Adds more details to output logging." + "description": "When true, add more details to output logging." }, "progress": { "type": "boolean", - "description": "Log progress to the console while building." + "description": "When true, log progress to the console while building." } }, "additionalProperties": false }, "extracti18n": { - "description": "Extract i18n target options for Build Facade.", + "description": "Extract i18n target options for Architect.", "type": "object", "properties": { "browserTarget": { @@ -1066,6 +1267,11 @@ "type": "string", "description": "Specifies the source language of the application." }, + "progress": { + "type": "boolean", + "description": "Log progress to the console.", + "default": true + }, "outputPath": { "type": "string", "description": "Path where output will be placed." @@ -1078,7 +1284,7 @@ "additionalProperties": false }, "karma": { - "description": "Karma target options for Build Facade.", + "description": "Karma target options for Architect.", "type": "object", "properties": { "main": { @@ -1141,9 +1347,34 @@ "description": "Defines the build environment." }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "progress": { "type": "boolean", @@ -1153,7 +1384,7 @@ "watch": { "type": "boolean", "description": "Run build when files change.", - "default": false + "default": true }, "poll": { "type": "number", @@ -1221,6 +1452,17 @@ ] }, "default": [] + }, + "reporters": { + "type": "array", + "description": "Karma reporters to use. Directly passed to the karma runner.", + "items": { + "type": "string" + } + }, + "webWorkerTsConfig": { + "type": "string", + "description": "TypeScript configuration for Web Worker modules." } }, "additionalProperties": false, @@ -1241,6 +1483,13 @@ "output": { "type": "string", "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -1273,6 +1522,11 @@ "type": "boolean", "description": "If the bundle will be lazy loaded.", "default": false + }, + "inject": { + "type": "boolean", + "description": "If the bundle will be referenced in the HTML file.", + "default": true } }, "additionalProperties": false, @@ -1289,7 +1543,7 @@ } }, "protractor": { - "description": "Protractor target options for Build Facade.", + "description": "Protractor target options for Architect.", "type": "object", "properties": { "protractorConfig": { @@ -1372,9 +1626,28 @@ "additionalProperties": false }, "optimization": { - "type": "boolean", "description": "Enables optimization of the build output.", - "default": false + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Enables optimization of the scripts output.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Enables optimization of the styles output.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fileReplacements": { "description": "Replace files with other files in the build.", @@ -1388,10 +1661,44 @@ "type": "string", "description": "Path where output will be placed." }, + "resourcesOutputPath": { + "type": "string", + "description": "The path where style resources will be placed, relative to outputPath." + }, "sourceMap": { - "type": "boolean", "description": "Output sourcemaps.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "scripts": { + "type": "boolean", + "description": "Output sourcemaps for all scripts.", + "default": true + }, + "styles": { + "type": "boolean", + "description": "Output sourcemaps for all styles.", + "default": true + }, + "hidden": { + "type": "boolean", + "description": "Output sourcemaps used for error reporting tools.", + "default": false + }, + "vendor": { + "type": "boolean", + "description": "Resolve vendor packages sourcemaps.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "vendorSourceMap": { "type": "boolean", @@ -1486,7 +1793,7 @@ }, "statsJson": { "type": "boolean", - "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse .", + "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", "default": false }, "forkTypeChecker": { @@ -1541,11 +1848,10 @@ } ] } - } }, "tslint": { - "description": "TSlint target options for Build Facade.", + "description": "TSlint target options for Architect.", "type": "object", "properties": { "tslintConfig": { diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 3a269fcee191..c1d3d71aa3c2 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -7,13 +7,14 @@ */ import 'symbol-observable'; // symbol polyfill must go first +// tslint:disable: no-console // tslint:disable-next-line:ordered-imports import-groups -import { tags, terminal } from '@angular-devkit/core'; -import { resolve } from '@angular-devkit/core/node'; +import { tags } from '@angular-devkit/core'; import * as fs from 'fs'; import * as path from 'path'; import { SemVer } from 'semver'; import { Duplex } from 'stream'; +import { colors } from '../utilities/color'; import { isWarningEnabled } from '../utilities/config'; const packageJson = require('../package.json'); @@ -40,12 +41,24 @@ function _fromPackageJson(cwd?: string) { return null; } - // Check if we need to profile this CLI run. if (process.env['NG_CLI_PROFILING']) { - const profiler = require('v8-profiler'); // tslint:disable-line:no-implicit-dependencies + let profiler: { + startProfiling: (name?: string, recsamples?: boolean) => void; + stopProfiling: (name?: string) => any; // tslint:disable-line:no-any + }; + try { + profiler = require('v8-profiler-node8'); // tslint:disable-line:no-implicit-dependencies + } catch (err) { + throw new Error( + `Could not require 'v8-profiler-node8'. You must install it separetely with ` + + `'npm install v8-profiler-node8 --no-save'.\n\nOriginal error:\n\n${err}`, + ); + } + profiler.startProfiling(); - const exitHandler = (options: { cleanup?: boolean, exit?: boolean }) => { + + const exitHandler = (options: { cleanup?: boolean; exit?: boolean }) => { if (options.cleanup) { const cpuProfile = profiler.stopProfiling(); fs.writeFileSync( @@ -66,14 +79,7 @@ if (process.env['NG_CLI_PROFILING']) { let cli; try { - const projectLocalCli = resolve( - '@angular/cli', - { - checkGlobal: false, - basedir: process.cwd(), - preserveSymlinks: true, - }, - ); + const projectLocalCli = require.resolve('@angular/cli', { paths: [process.cwd()] }); // This was run from a global, check local version. const globalVersion = new SemVer(packageJson['version']); @@ -90,7 +96,7 @@ try { } if (shouldWarn && isWarningEnabled('versionMismatch')) { - const warning = terminal.yellow(tags.stripIndents` + const warning = colors.yellow(tags.stripIndents` Your global Angular CLI version (${globalVersion}) is greater than your local version (${localVersion}). The local Angular CLI version is used. @@ -98,10 +104,10 @@ try { `); // Don't show warning colorised on `ng completion` if (process.argv[2] !== 'completion') { - // eslint-disable-next-line no-console + // eslint-disable-next-line no-console console.error(warning); } else { - // eslint-disable-next-line no-console + // eslint-disable-next-line no-console console.error(warning); process.exit(1); } diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts new file mode 100644 index 000000000000..db8619792f3e --- /dev/null +++ b/packages/angular/cli/models/analytics.ts @@ -0,0 +1,602 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { analytics, json, tags } from '@angular-devkit/core'; +import * as child_process from 'child_process'; +import * as debug from 'debug'; +import { writeFileSync } from 'fs'; +import * as inquirer from 'inquirer'; +import * as os from 'os'; +import * as ua from 'universal-analytics'; +import { v4 as uuidV4 } from 'uuid'; +import { colors } from '../utilities/color'; +import { getWorkspace, getWorkspaceRaw } from '../utilities/config'; +import { isTTY } from '../utilities/tty'; + +// tslint:disable: no-console +const analyticsDebug = debug('ng:analytics'); // Generate analytics, including settings and users. +const analyticsLogDebug = debug('ng:analytics:log'); // Actual logs of events. + +const BYTES_PER_GIGABYTES = 1024 * 1024 * 1024; + +let _defaultAngularCliPropertyCache: string; +export const AnalyticsProperties = { + AngularCliProd: 'UA-8594346-29', + AngularCliStaging: 'UA-8594346-32', + get AngularCliDefault(): string { + if (_defaultAngularCliPropertyCache) { + return _defaultAngularCliPropertyCache; + } + + const v = require('../package.json').version; + + // The logic is if it's a full version then we should use the prod GA property. + if (/^\d+\.\d+\.\d+$/.test(v) && v !== '0.0.0') { + _defaultAngularCliPropertyCache = AnalyticsProperties.AngularCliProd; + } else { + _defaultAngularCliPropertyCache = AnalyticsProperties.AngularCliStaging; + } + + return _defaultAngularCliPropertyCache; + }, +}; + +/** + * This is the ultimate safelist for checking if a package name is safe to report to analytics. + */ +export const analyticsPackageSafelist = [ + /^@angular\//, + /^@angular-devkit\//, + /^@ngtools\//, + '@schematics/angular', + '@schematics/schematics', + '@schematics/update', +]; + +export function isPackageNameSafeForAnalytics(name: string) { + return analyticsPackageSafelist.some(pattern => { + if (typeof pattern == 'string') { + return pattern === name; + } else { + return pattern.test(name); + } + }); +} + +/** + * Attempt to get the Windows Language Code string. + * @private + */ +function _getWindowsLanguageCode(): string | undefined { + if (!os.platform().startsWith('win')) { + return undefined; + } + + try { + // This is true on Windows XP, 7, 8 and 10 AFAIK. Would return empty string or fail if it + // doesn't work. + return child_process + .execSync('wmic.exe os get locale') + .toString() + .trim(); + } catch (_) {} + + return undefined; +} + +/** + * Get a language code. + * @private + */ +function _getLanguage() { + // Note: Windows does not expose the configured language by default. + return ( + process.env.LANG || // Default Unix env variable. + process.env.LC_CTYPE || // For C libraries. Sometimes the above isn't set. + process.env.LANGSPEC || // For Windows, sometimes this will be set (not always). + _getWindowsLanguageCode() || + '??' + ); // ¯\_(ツ)_/¯ +} + +/** + * Return the number of CPUs. + * @private + */ +function _getCpuCount() { + const cpus = os.cpus(); + + // Return "(count)x(average speed)". + return cpus.length; +} + +/** + * Get the first CPU's speed. It's very rare to have multiple CPUs of different speed (in most + * non-ARM configurations anyway), so that's all we care about. + * @private + */ +function _getCpuSpeed() { + const cpus = os.cpus(); + + return Math.floor(cpus[0].speed); +} + +/** + * Get the amount of memory, in megabytes. + * @private + */ +function _getRamSize() { + // Report in gigabytes (or closest). Otherwise it's too much noise. + return Math.round(os.totalmem() / BYTES_PER_GIGABYTES); +} + +/** + * Get the Node name and version. This returns a string like "Node 10.11", or "io.js 3.5". + * @private + */ +function _getNodeVersion() { + // We use any here because p.release is a new Node construct in Node 10 (and our typings are the + // minimal version of Node we support). + const p = process as any; // tslint:disable-line:no-any + const name = + (typeof p.release == 'object' && typeof p.release.name == 'string' && p.release.name) || + process.argv0; + + return name + ' ' + process.version; +} + +/** + * Get a numerical MAJOR.MINOR version of node. We report this as a metric. + * @private + */ +function _getNumericNodeVersion() { + const p = process.version; + const m = p.match(/\d+\.\d+/); + + return (m && m[0] && parseFloat(m[0])) || 0; +} + +// These are just approximations of UA strings. We just try to fool Google Analytics to give us the +// data we want. +// See https://developers.whatismybrowser.com/useragents/ +const osVersionMap: { [os: string]: { [release: string]: string } } = { + darwin: { + '1.3.1': '10_0_4', + '1.4.1': '10_1_0', + '5.1': '10_1_1', + '5.2': '10_1_5', + '6.0.1': '10_2', + '6.8': '10_2_8', + '7.0': '10_3_0', + '7.9': '10_3_9', + '8.0': '10_4_0', + '8.11': '10_4_11', + '9.0': '10_5_0', + '9.8': '10_5_8', + '10.0': '10_6_0', + '10.8': '10_6_8', + // We stop here because we try to math out the version for anything greater than 10, and it + // works. Those versions are standardized using a calculation now. + }, + win32: { + '6.3.9600': 'Windows 8.1', + '6.2.9200': 'Windows 8', + '6.1.7601': 'Windows 7 SP1', + '6.1.7600': 'Windows 7', + '6.0.6002': 'Windows Vista SP2', + '6.0.6000': 'Windows Vista', + '5.1.2600': 'Windows XP', + }, +}; + +/** + * Build a fake User Agent string for OSX. This gets sent to Analytics so it shows the proper OS, + * versions and others. + * @private + */ +function _buildUserAgentStringForOsx() { + let v = osVersionMap.darwin[os.release()]; + + if (!v) { + // Remove 4 to tie Darwin version to OSX version, add other info. + const x = parseFloat(os.release()); + if (x > 10) { + v = `10_` + (x - 4).toString().replace('.', '_'); + } + } + + const cpuModel = os.cpus()[0].model.match(/^[a-z]+/i); + const cpu = cpuModel ? cpuModel[0] : os.cpus()[0].model; + + return `(Macintosh; ${cpu} Mac OS X ${v || os.release()})`; +} + +/** + * Build a fake User Agent string for Windows. This gets sent to Analytics so it shows the proper + * OS, versions and others. + * @private + */ +function _buildUserAgentStringForWindows() { + return `(Windows NT ${os.release()})`; +} + +/** + * Build a fake User Agent string for Linux. This gets sent to Analytics so it shows the proper OS, + * versions and others. + * @private + */ +function _buildUserAgentStringForLinux() { + return `(X11; Linux i686; ${os.release()}; ${os.cpus()[0].model})`; +} + +/** + * Build a fake User Agent string. This gets sent to Analytics so it shows the proper OS version. + * @private + */ +function _buildUserAgentString() { + switch (os.platform()) { + case 'darwin': + return _buildUserAgentStringForOsx(); + + case 'win32': + return _buildUserAgentStringForWindows(); + + case 'linux': + return _buildUserAgentStringForLinux(); + + default: + return os.platform() + ' ' + os.release(); + } +} + +/** + * Implementation of the Analytics interface for using `universal-analytics` package. + */ +export class UniversalAnalytics implements analytics.Analytics { + private _ua: ua.Visitor; + private _dirty = false; + private _metrics: (string | number)[] = []; + private _dimensions: (string | number)[] = []; + + /** + * @param trackingId The Google Analytics ID. + * @param uid A User ID. + */ + constructor(trackingId: string, uid: string) { + this._ua = ua(trackingId, uid, { + enableBatching: true, + batchSize: 5, + }); + + // Add persistent params for appVersion. + this._ua.set('ds', 'cli'); + this._ua.set('ua', _buildUserAgentString()); + this._ua.set('ul', _getLanguage()); + + // @angular/cli with version. + this._ua.set('an', require('../package.json').name); + this._ua.set('av', require('../package.json').version); + + // We use the application ID for the Node version. This should be "node 10.10.0". + // We also use a custom metrics, but + this._ua.set('aid', _getNodeVersion()); + + // We set custom metrics for values we care about. + this._dimensions[analytics.NgCliAnalyticsDimensions.CpuCount] = _getCpuCount(); + this._dimensions[analytics.NgCliAnalyticsDimensions.CpuSpeed] = _getCpuSpeed(); + this._dimensions[analytics.NgCliAnalyticsDimensions.RamInGigabytes] = _getRamSize(); + this._dimensions[analytics.NgCliAnalyticsDimensions.NodeVersion] = _getNumericNodeVersion(); + } + + /** + * Creates the dimension and metrics variables to pass to universal-analytics. + * @private + */ + private _customVariables(options: analytics.CustomDimensionsAndMetricsOptions) { + const additionals: { [key: string]: boolean | number | string } = {}; + this._dimensions.forEach((v, i) => (additionals['cd' + i] = v)); + (options.dimensions || []).forEach((v, i) => (additionals['cd' + i] = v)); + this._metrics.forEach((v, i) => (additionals['cm' + i] = v)); + (options.metrics || []).forEach((v, i) => (additionals['cm' + i] = v)); + + return additionals; + } + + event(ec: string, ea: string, options: analytics.EventOptions = {}) { + const vars = this._customVariables(options); + analyticsLogDebug('event ec=%j, ea=%j, %j', ec, ea, vars); + + const { label: el, value: ev } = options; + this._dirty = true; + this._ua.event({ ec, ea, el, ev, ...vars }); + } + screenview(cd: string, an: string, options: analytics.ScreenviewOptions = {}) { + const vars = this._customVariables(options); + analyticsLogDebug('screenview cd=%j, an=%j, %j', cd, an, vars); + + const { appVersion: av, appId: aid, appInstallerId: aiid } = options; + this._dirty = true; + this._ua.screenview({ cd, an, av, aid, aiid, ...vars }); + } + pageview(dp: string, options: analytics.PageviewOptions = {}) { + const vars = this._customVariables(options); + analyticsLogDebug('pageview dp=%j, %j', dp, vars); + + const { hostname: dh, title: dt } = options; + this._dirty = true; + this._ua.pageview({ dp, dh, dt, ...vars }); + } + timing(utc: string, utv: string, utt: string | number, options: analytics.TimingOptions = {}) { + const vars = this._customVariables(options); + analyticsLogDebug('timing utc=%j, utv=%j, utl=%j, %j', utc, utv, utt, vars); + + const { label: utl } = options; + this._dirty = true; + this._ua.timing({ utc, utv, utt, utl, ...vars }); + } + + flush(): Promise { + if (!this._dirty) { + return Promise.resolve(); + } + + this._dirty = false; + + return new Promise(resolve => this._ua.send(resolve)); + } +} + +/** + * Set analytics settings. This does not work if the user is not inside a project. + * @param level Which config to use. "global" for user-level, and "local" for project-level. + * @param value Either a user ID, true to generate a new User ID, or false to disable analytics. + */ +export function setAnalyticsConfig(level: 'global' | 'local', value: string | boolean) { + analyticsDebug('setting %s level analytics to: %s', level, value); + const [config, configPath] = getWorkspaceRaw(level); + if (!config || !configPath) { + throw new Error(`Could not find ${level} workspace.`); + } + + const configValue = config.value; + const cli: json.JsonValue = configValue['cli'] || (configValue['cli'] = {}); + + if (!json.isJsonObject(cli)) { + throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`); + } + + if (value === true) { + value = uuidV4(); + } + cli['analytics'] = value; + + const output = JSON.stringify(configValue, null, 2); + writeFileSync(configPath, output); + analyticsDebug('done'); +} + +/** + * Prompt the user for usage gathering permission. + * @param force Whether to ask regardless of whether or not the user is using an interactive shell. + * @return Whether or not the user was shown a prompt. + */ +export async function promptGlobalAnalytics(force = false) { + analyticsDebug('prompting global analytics.'); + if (force || isTTY()) { + const answers = await inquirer.prompt<{ analytics: boolean }>([ + { + type: 'confirm', + name: 'analytics', + message: tags.stripIndents` + Would you like to share anonymous usage data with the Angular Team at Google under + Google’s Privacy Policy at https://policies.google.com/privacy? For more details and + how to change this setting, see http://angular.io/analytics. + `, + default: false, + }, + ]); + + setAnalyticsConfig('global', answers.analytics); + + if (answers.analytics) { + console.log(''); + console.log(tags.stripIndent` + Thank you for sharing anonymous usage data. If you change your mind, the following + command will disable this feature entirely: + + ${colors.yellow('ng analytics off')} + `); + console.log(''); + + // Send back a ping with the user `optin`. + const ua = new UniversalAnalytics(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 UniversalAnalytics(AnalyticsProperties.AngularCliDefault, 'optout'); + ua.pageview('/telemetry/optout'); + await ua.flush(); + } + + return true; + } else { + analyticsDebug('Either STDOUT or STDIN are not TTY and we skipped the prompt.'); + } + + return false; +} + +/** + * Prompt the user for usage gathering permission for the local project. Fails if there is no + * local workspace. + * @param force Whether to ask regardless of whether or not the user is using an interactive shell. + * @return Whether or not the user was shown a prompt. + */ +export async function promptProjectAnalytics(force = false): Promise { + analyticsDebug('prompting user'); + const [config, configPath] = getWorkspaceRaw('local'); + if (!config || !configPath) { + throw new Error(`Could not find a local workspace. Are you in a project?`); + } + + if (force || isTTY()) { + const answers = await inquirer.prompt<{ analytics: boolean }>([ + { + type: 'confirm', + name: 'analytics', + message: tags.stripIndents` + Would you like to share anonymous usage data about this project with the Angular Team at + Google under Google’s Privacy Policy at https://policies.google.com/privacy? For more + details and how to change this setting, see http://angular.io/analytics. + + `, + default: false, + }, + ]); + + setAnalyticsConfig('local', answers.analytics); + + if (answers.analytics) { + console.log(''); + console.log(tags.stripIndent` + Thank you for sharing anonymous usage data. Would you change your mind, the following + command will disable this feature entirely: + + ${colors.yellow('ng analytics project off')} + `); + console.log(''); + + // Send back a ping with the user `optin`. + const ua = new UniversalAnalytics(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 UniversalAnalytics(AnalyticsProperties.AngularCliDefault, 'optout'); + ua.pageview('/telemetry/project/optout'); + await ua.flush(); + } + + return true; + } + + return false; +} + +export function hasGlobalAnalyticsConfiguration(): boolean { + try { + const globalWorkspace = getWorkspace('global'); + const analyticsConfig: string | undefined | null | { uid?: string } = + globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics']; + + if (analyticsConfig !== null && analyticsConfig !== undefined) { + return true; + } + } catch {} + + return false; +} + +/** + * Get the global analytics object for the user. This returns an instance of UniversalAnalytics, + * or undefined if analytics are disabled. + * + * If any problem happens, it is considered the user has been opting out of analytics. + */ +export function getGlobalAnalytics(): UniversalAnalytics | undefined { + analyticsDebug('getGlobalAnalytics'); + const propertyId = AnalyticsProperties.AngularCliDefault; + + if ('NG_CLI_ANALYTICS' in process.env) { + if (process.env['NG_CLI_ANALYTICS'] == 'false' || process.env['NG_CLI_ANALYTICS'] == '') { + analyticsDebug('NG_CLI_ANALYTICS is false'); + + return undefined; + } + if (process.env['NG_CLI_ANALYTICS'] === 'ci') { + analyticsDebug('Running in CI mode'); + + return new UniversalAnalytics(propertyId, 'ci'); + } + } + + // If anything happens we just keep the NOOP analytics. + try { + const globalWorkspace = getWorkspace('global'); + const analyticsConfig: string | undefined | null | { uid?: string } = + globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics']; + analyticsDebug('Client Analytics config found: %j', analyticsConfig); + + if (analyticsConfig === false) { + analyticsDebug('Analytics disabled. Ignoring all analytics.'); + + return undefined; + } else if (analyticsConfig === undefined || analyticsConfig === null) { + analyticsDebug('Analytics settings not found. Ignoring all analytics.'); + + // globalWorkspace can be null if there is no file. analyticsConfig would be null in this + // case. Since there is no file, the user hasn't answered and the expected return value is + // undefined. + return undefined; + } else { + let uid: string | undefined = undefined; + if (typeof analyticsConfig == 'string') { + uid = analyticsConfig; + } else if (typeof analyticsConfig == 'object' && typeof analyticsConfig['uid'] == 'string') { + uid = analyticsConfig['uid']; + } + + analyticsDebug('client id: %j', uid); + if (uid == undefined) { + return undefined; + } + + return new UniversalAnalytics(propertyId, uid); + } + } catch (err) { + analyticsDebug('Error happened during reading of analytics config: %s', err.message); + + return undefined; + } +} + +/** + * Return the usage analytics sharing setting, which is either a property string (GA-XXXXXXX-XX), + * or undefined if no sharing. + */ +export function getSharedAnalytics(): UniversalAnalytics | undefined { + analyticsDebug('getSharedAnalytics'); + + const envVarName = 'NG_CLI_ANALYTICS_SHARE'; + if (envVarName in process.env) { + if (process.env[envVarName] == 'false' || process.env[envVarName] == '') { + analyticsDebug('NG_CLI_ANALYTICS is false'); + + return undefined; + } + } + + // If anything happens we just keep the NOOP analytics. + try { + const globalWorkspace = getWorkspace('global'); + const analyticsConfig = + globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analyticsSharing']; + + if (!analyticsConfig || !analyticsConfig.tracking || !analyticsConfig.uuid) { + return undefined; + } else { + analyticsDebug('Analytics sharing info: %j', analyticsConfig); + + return new UniversalAnalytics(analyticsConfig.tracking, analyticsConfig.uuid); + } + } catch (err) { + analyticsDebug('Error happened during reading of analytics sharing config: %s', err.message); + + return undefined; + } +} diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index aacacfe9a588..d8250fbdede5 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -5,206 +5,301 @@ * Use 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, - BuildEvent, - BuilderDescription, - TargetSpecifier, -} from '@angular-devkit/architect'; -import { - JsonObject, - UnknownException, - experimental, - schema, - strings, - tags, -} from '@angular-devkit/core'; -import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; -import { from, of } from 'rxjs'; -import { concatMap, map, tap, toArray } from 'rxjs/operators'; -import { Command, Option } from './command'; -import { WorkspaceLoader } from './workspace-loader'; - -export interface ProjectAndConfigurationOptions { +import { Architect, Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { json, schema, tags, workspaces } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { BepJsonWriter } from '../utilities/bep'; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; +import { isPackageNameSafeForAnalytics } from './analytics'; +import { BaseCommandOptions, Command } from './command'; +import { Arguments, Option } from './interface'; +import { parseArguments } from './parser'; + +export interface ArchitectCommandOptions extends BaseCommandOptions { project?: string; configuration?: string; - prod: boolean; -} - -export interface TargetOptions { + prod?: boolean; target?: string; } -export type ArchitectCommandOptions = ProjectAndConfigurationOptions & TargetOptions & JsonObject; - -export abstract class ArchitectCommand extends Command { +export abstract class ArchitectCommand< + T extends ArchitectCommandOptions = ArchitectCommandOptions +> extends Command { + protected _architect: Architect; + protected _architectHost: WorkspaceNodeModulesArchitectHost; + protected _workspace: workspaces.WorkspaceDefinition; + protected _registry: json.schema.SchemaRegistry; - private _host = new NodeJsSyncHost(); - private _architect: Architect; - private _workspace: experimental.workspace.Workspace; - private _logger = createConsoleLogger(); // If this command supports running multiple targets. protected multiTarget = false; - readonly Options: Option[] = [{ - name: 'configuration', - description: 'The configuration', - type: 'string', - aliases: ['c'], - }]; + target: string | undefined; - readonly arguments = ['project']; + public async initialize(options: T & Arguments): Promise { + await super.initialize(options); - target: string | undefined; + this._registry = new json.schema.CoreSchemaRegistry(); + this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - public async initialize(options: ArchitectCommandOptions): Promise { - return this._loadWorkspaceAndArchitect().pipe( - concatMap(() => { - const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); - - if (this.target && !targetSpec.project) { - const projects = this.getProjectNamesByTarget(this.target); - - if (projects.length === 1) { - // If there is a single target, use it to parse overrides. - targetSpec.project = projects[0]; - } else { - // Multiple targets can have different, incompatible options. - // We only lookup options for single targets. - return of(null); - } - } + const { workspace } = await workspaces.readWorkspace( + this.workspace.root, + workspaces.createWorkspaceHost(new NodeJsSyncHost()), + ); + this._workspace = workspace; - if (!targetSpec.project || !targetSpec.target) { - throw new Error('Cannot determine project or target for Architect command.'); - } + this._architectHost = new WorkspaceNodeModulesArchitectHost(workspace, this.workspace.root); + this._architect = new Architect(this._architectHost, this._registry); + + if (!this.target) { + if (options.help) { + // This is a special case where we just return. + return; + } + + const specifier = this._makeTargetSpecifier(options); + if (!specifier.project || !specifier.target) { + throw new Error('Cannot determine project or target for command.'); + } + + return; + } + + const commandLeftovers = options['--']; + let projectName = options.project; + const targetProjectNames: string[] = []; + for (const [name, project] of this._workspace.projects) { + if (project.targets.has(this.target)) { + targetProjectNames.push(name); + } + } + + if (targetProjectNames.length === 0) { + throw new Error(`No projects support the '${this.target}' target.`); + } - const builderConfig = this._architect.getBuilderConfiguration(targetSpec); + if (projectName && !targetProjectNames.includes(projectName)) { + throw new Error(`Project '${projectName}' does not support the '${this.target}' target.`); + } - return this._architect.getBuilderDescription(builderConfig).pipe( - tap(builderDesc => { this.mapArchitectOptions(builderDesc.schema); }), + 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, ); - }), - ).toPromise() - .then(() => { }); - } + const parsedOptions = parseArguments([...commandLeftovers], optionDefs); + const builderLeftovers = parsedOptions['--'] || []; + leftoverMap.set(name, { optionDefs, parsedOptions }); - public validate(options: ArchitectCommandOptions) { - if (!options.project && this.target) { - const projectNames = this.getProjectNamesByTarget(this.target); - const { overrides } = this._makeTargetSpecifier(options); - if (projectNames.length > 1 && Object.keys(overrides || {}).length > 0) { - // Verify that all builders are the same, otherwise error out (since the meaning of an - // option could vary from builder to builder). - - const builders: string[] = []; - for (const projectName of projectNames) { - const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); - const targetDesc = this._architect.getBuilderConfiguration({ - project: projectName, - target: targetSpec.target, - }); - - if (builders.indexOf(targetDesc.builder) == -1) { - builders.push(targetDesc.builder); + potentialProjectNames = new Set(builderLeftovers.filter(x => potentialProjectNames.has(x))); + } + + if (potentialProjectNames.size === 1) { + projectName = [...potentialProjectNames][0]; + + // remove the project name from the leftovers + const optionInfo = leftoverMap.get(projectName); + if (optionInfo) { + const locations = []; + let i = 0; + while (i < commandLeftovers.length) { + i = commandLeftovers.indexOf(projectName, i + 1); + if (i === -1) { + break; + } + locations.push(i); + } + delete optionInfo.parsedOptions['--']; + for (const location of locations) { + const tempLeftovers = [...commandLeftovers]; + tempLeftovers.splice(location, 1); + const tempArgs = parseArguments([...tempLeftovers], optionInfo.optionDefs); + delete tempArgs['--']; + if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) { + options['--'] = tempLeftovers; + break; + } } } + } + + if (!projectName && this.multiTarget && builderNames.size > 1) { + throw new Error(tags.oneLine` + Architect commands with command line overrides cannot target different builders. The + '${this.target}' target would run on projects ${targetProjectNames.join()} which have the + following builders: ${'\n ' + [...builderNames].join('\n ')} + `); + } + } + + if (!projectName && !this.multiTarget) { + const defaultProjectName = this._workspace.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 { + throw new Error('Cannot determine project or target for command.'); + } + } + + options.project = projectName; - if (builders.length > 1) { - throw new Error(tags.oneLine` - Architect commands with command line overrides cannot target different builders. The - '${this.target}' target would run on projects ${projectNames.join()} which have the - following builders: ${'\n ' + builders.join('\n ')} - `); + const builderConf = await this._architectHost.getBuilderNameForTarget({ + project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''), + target: this.target, + }); + const builderDesc = await this._architectHost.resolveBuilder(builderConf); + + this.description.options.push( + ...(await parseJsonSchemaToOptions( + this._registry, + builderDesc.optionSchema as json.JsonObject, + )), + ); + + // Update options to remove analytics from options if the builder isn't safelisted. + for (const o of this.description.options) { + if (o.userAnalytics) { + if (!isPackageNameSafeForAnalytics(builderConf)) { + o.userAnalytics = undefined; } } } + } - return true; + async run(options: ArchitectCommandOptions & Arguments) { + return await this.runArchitectTarget(options); } - protected mapArchitectOptions(schema: JsonObject) { - const properties = schema.properties; - if (typeof properties != 'object' || properties === null || Array.isArray(properties)) { - throw new UnknownException('Invalid schema.'); - } - const keys = Object.keys(properties); - keys - .map(key => { - const value = properties[key]; - if (typeof value != 'object') { - throw new UnknownException('Invalid schema.'); - } + protected async runBepTarget( + command: string, + configuration: Target, + overrides: json.JsonObject, + buildEventLog: string, + ): Promise { + const bep = new BepJsonWriter(buildEventLog); + + // Send start + bep.writeBuildStarted(command); + + let last = 1; + let rebuild = false; + const run = await this._architect.scheduleTarget(configuration, overrides, { + logger: this.logger, + }); + await run.output.forEach(event => { + last = event.success ? 0 : 1; + + if (rebuild) { + // NOTE: This will have an incorrect timestamp but this cannot be fixed + // until builders report additional status events + bep.writeBuildStarted(command); + } else { + rebuild = true; + } - return { - ...value, - name: strings.dasherize(key), - } as any; // tslint:disable-line:no-any - }) - .map(opt => { - const types = ['string', 'boolean', 'integer', 'number']; - // Ignore arrays / objects. - if (types.indexOf(opt.type) === -1) { - return null; - } + bep.writeBuildFinished(last); + }); - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x) - .forEach(option => this.addOptions(option)); + await run.stop(); + + return last; } - protected prodOption: Option = { - name: 'prod', - description: 'Flag to set configuration to "prod".', - type: 'boolean', - }; - - protected configurationOption: Option = { - name: 'configuration', - description: 'Specify the configuration to use.', - type: 'string', - aliases: ['c'], - }; - - protected async runArchitectTarget(options: ArchitectCommandOptions): Promise { - delete options._; - const targetSpec = this._makeTargetSpecifier(options); - - const runSingleTarget = (targetSpec: TargetSpecifier) => this._architect.run( - this._architect.getBuilderConfiguration(targetSpec), - { logger: this._logger }, - ).pipe( - map((buildEvent: BuildEvent) => buildEvent.success ? 0 : 1), + protected async runSingleTarget( + target: Target, + targetOptions: string[], + commandOptions: ArchitectCommandOptions & Arguments, + ) { + // We need to build the builderSpec twice because architect does not understand + // overrides separately (getting the configuration builds the whole project, including + // overrides). + const builderConf = await this._architectHost.getBuilderNameForTarget(target); + const builderDesc = await this._architectHost.resolveBuilder(builderConf); + const targetOptionArray = await parseJsonSchemaToOptions( + this._registry, + builderDesc.optionSchema as json.JsonObject, ); + const overrides = parseArguments(targetOptions, targetOptionArray, this.logger); + + const allowAdditionalProperties = + typeof builderDesc.optionSchema === 'object' && builderDesc.optionSchema.additionalProperties; + + if (overrides['--'] && !allowAdditionalProperties) { + (overrides['--'] || []).forEach(additional => { + this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); + }); + + return 1; + } + + if (commandOptions.buildEventLog && ['build', 'serve'].includes(this.description.name)) { + // The build/serve commands supports BEP messaging + this.logger.warn('BEP support is experimental and subject to change.'); + + return this.runBepTarget( + this.description.name, + target, + overrides as json.JsonObject, + commandOptions.buildEventLog as string, + ); + } else { + const run = await this._architect.scheduleTarget(target, overrides as json.JsonObject, { + logger: this.logger, + analytics: isPackageNameSafeForAnalytics(builderConf) ? this.analytics : undefined, + }); + + const { error, success } = await run.output.toPromise(); + await run.stop(); + + if (error) { + this.logger.error(error); + } + + return success ? 0 : 1; + } + } + + protected async runArchitectTarget( + options: ArchitectCommandOptions & Arguments, + ): Promise { + const extra = options['--'] || []; try { + const targetSpec = this._makeTargetSpecifier(options); if (!targetSpec.project && this.target) { // This runs each target sequentially. // Running them in parallel would jumble the log messages. - return await from(this.getProjectNamesByTarget(this.target)).pipe( - concatMap(project => runSingleTarget({ ...targetSpec, project })), - toArray(), - map(results => results.every(res => res === 0) ? 0 : 1), - ) - .toPromise(); + let result = 0; + for (const project of this.getProjectNamesByTarget(this.target)) { + result |= await this.runSingleTarget( + { ...targetSpec, project } as Target, + extra, + options, + ); + } + + return result; } else { - return await runSingleTarget(targetSpec).toPromise(); + return await this.runSingleTarget(targetSpec, extra, options); } } catch (e) { if (e instanceof schema.SchemaValidationException) { @@ -233,17 +328,20 @@ export abstract class ArchitectCommand extends Command } private getProjectNamesByTarget(targetName: string): string[] { - const allProjectsForTargetName = this._workspace.listProjectNames().map(projectName => - this._architect.listProjectTargets(projectName).includes(targetName) ? projectName : null, - ).filter(x => !!x) as string[]; + const allProjectsForTargetName: string[] = []; + 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 try the default project project first, + // For single target commands, we try the default project first, // then the full list if it has a single project, then error out. - const maybeDefaultProject = this._workspace.getDefaultProjectName(); + const maybeDefaultProject = this._workspace.extensions['defaultProject'] as string; if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) { return [maybeDefaultProject]; } @@ -256,44 +354,22 @@ export abstract class ArchitectCommand extends Command } } - private _loadWorkspaceAndArchitect() { - const workspaceLoader = new WorkspaceLoader(this._host); - - return workspaceLoader.loadWorkspace(this.project.root).pipe( - tap((workspace: experimental.workspace.Workspace) => this._workspace = workspace), - concatMap((workspace: experimental.workspace.Workspace) => { - return new Architect(workspace).loadArchitect(); - }), - tap((architect: Architect) => this._architect = architect), - ); - } + private _makeTargetSpecifier(commandOptions: ArchitectCommandOptions): Target { + let project, target, configuration; - private _makeTargetSpecifier(options: ArchitectCommandOptions): TargetSpecifier { - let project, target, configuration, overrides; + if (commandOptions.target) { + [project, target, configuration] = commandOptions.target.split(':'); - if (options.target) { - [project, target, configuration] = options.target.split(':'); - - overrides = { ...options }; - delete overrides.target; - - if (overrides.configuration) { - configuration = overrides.configuration; - delete overrides.configuration; + if (commandOptions.configuration) { + configuration = commandOptions.configuration; } } else { - project = options.project; + project = commandOptions.project; target = this.target; - configuration = options.configuration; - if (!configuration && options.prod) { + configuration = commandOptions.configuration; + if (!configuration && commandOptions.prod) { configuration = 'production'; } - - overrides = { ...options }; - - delete overrides.configuration; - delete overrides.prod; - delete overrides.project; } if (!project) { @@ -305,9 +381,8 @@ export abstract class ArchitectCommand extends Command return { project, - configuration, + configuration: configuration || '', target, - overrides, }; } } diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index 8792328df6f1..58387fcef775 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -5,454 +5,238 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { - JsonObject, - deepCopy, + JsonParseMode, + analytics, + isJsonObject, + json, logging, - parseJson, schema, - strings as coreStrings, + strings, tags, } from '@angular-devkit/core'; -import { ExportStringRef } from '@angular-devkit/schematics/tools'; +import * as debug from 'debug'; import { readFileSync } from 'fs'; -import { dirname, join } from 'path'; -import { of, throwError } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; -import * as yargsParser from 'yargs-parser'; -import { - Command, - CommandConstructor, - CommandContext, - CommandScope, - Option, -} from '../models/command'; -import { findUp } from '../utilities/find-up'; -import { insideProject } from '../utilities/project'; -import { convertSchemaToOptions, parseSchema } from './json-schema'; - - -interface CommandMap { +import { join, resolve } from 'path'; +import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; +import { UniversalAnalytics, getGlobalAnalytics, getSharedAnalytics } from './analytics'; +import { Command } from './command'; +import { CommandDescription, CommandWorkspace } from './interface'; +import * as parser from './parser'; + +const analyticsDebug = debug('ng:analytics:commands'); + +// NOTE: Update commands.json if changing this. It's still deep imported in one CI validation +const standardCommands = { + 'add': '../commands/add.json', + 'analytics': '../commands/analytics.json', + 'build': '../commands/build.json', + 'config': '../commands/config.json', + 'doc': '../commands/doc.json', + 'e2e': '../commands/e2e.json', + 'make-this-awesome': '../commands/easter-egg.json', + 'generate': '../commands/generate.json', + 'get': '../commands/deprecated.json', + 'set': '../commands/deprecated.json', + 'help': '../commands/help.json', + 'lint': '../commands/lint.json', + 'new': '../commands/new.json', + 'run': '../commands/run.json', + 'serve': '../commands/serve.json', + 'test': '../commands/test.json', + 'update': '../commands/update.json', + 'version': '../commands/version.json', + 'xi18n': '../commands/xi18n.json', +}; + +export interface CommandMapOptions { [key: string]: string; } -interface CommandMetadata { - description: string; - $aliases?: string[]; - $impl: string; - $scope?: 'in' | 'out'; - $type?: 'architect' | 'schematic'; - $hidden?: boolean; -} - -interface CommandLocation { - path: string; - text: string; - rawData: CommandMetadata; +/** + * Create the analytics instance. + * @private + */ +async function _createAnalytics(): Promise { + const config = getGlobalAnalytics(); + const maybeSharedAnalytics = getSharedAnalytics(); + + if (config && maybeSharedAnalytics) { + return new analytics.MultiAnalytics([config, maybeSharedAnalytics]); + } else if (config) { + return config; + } else if (maybeSharedAnalytics) { + return maybeSharedAnalytics; + } else { + return new analytics.NoopAnalytics(); + } } -// Based off https://en.wikipedia.org/wiki/Levenshtein_distance -// No optimization, really. -function levenshtein(a: string, b: string): number { - /* base case: empty strings */ - if (a.length == 0) { - return b.length; - } - if (b.length == 0) { - return a.length; +async function loadCommandDescription( + name: string, + path: string, + registry: json.schema.CoreSchemaRegistry, +): Promise { + const schemaPath = resolve(__dirname, path); + const schemaContent = readFileSync(schemaPath, 'utf-8'); + const schema = json.parseJson(schemaContent, JsonParseMode.Loose, { path: schemaPath }); + if (!isJsonObject(schema)) { + throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); } - // Test if last characters of the strings match. - const cost = a[a.length - 1] == b[b.length - 1] ? 0 : 1; - - /* return minimum of delete char from s, delete char from t, and delete char from both */ - return Math.min( - levenshtein(a.slice(0, -1), b) + 1, - levenshtein(a, b.slice(0, -1)) + 1, - levenshtein(a.slice(0, -1), b.slice(0, -1)) + cost, - ); + return parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema); } /** * Run a command. * @param args Raw unparsed arguments. * @param logger The logger to use. - * @param context Execution context. + * @param workspace Workspace information. + * @param commands The map of supported commands. + * @param options Additional options. */ export async function runCommand( args: string[], logger: logging.Logger, - context: CommandContext, - commandMap?: CommandMap, + workspace: CommandWorkspace, + commands: CommandMapOptions = standardCommands, + options: { analytics?: analytics.Analytics } = {}, ): Promise { + // This registry is exclusively used for flattening schemas, and not for validating. + const registry = new schema.CoreSchemaRegistry([]); + registry.registerUriHandler((uri: string) => { + if (uri.startsWith('ng-cli://')) { + const content = readFileSync(join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); - // if not args supplied, just run the help command. - if (!args || args.length === 0) { - args = ['help']; - } - const rawOptions = yargsParser(args, { alias: { help: ['h'] }, boolean: [ 'help' ] }); - let commandName = rawOptions._[0] || ''; - - // remove the command name - rawOptions._ = rawOptions._.slice(1); - const executionScope = insideProject() - ? CommandScope.inProject - : CommandScope.outsideProject; - - if (commandMap === undefined) { - const commandMapPath = findUp('commands.json', __dirname); - if (commandMapPath === null) { - logger.fatal('Unable to find command map.'); - - return 1; - } - const cliDir = dirname(commandMapPath); - const commandsText = readFileSync(commandMapPath).toString('utf-8'); - const commandJson = JSON.parse(commandsText) as { [name: string]: string }; - - commandMap = {}; - for (const commandName of Object.keys(commandJson)) { - commandMap[commandName] = join(cliDir, commandJson[commandName]); + return Promise.resolve(JSON.parse(content)); + } else { + return null; } - } + }); - let commandMetadata = commandName ? findCommand(commandMap, commandName) : null; + let commandName: string | undefined = undefined; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; - if (!commandMetadata && (rawOptions.v || rawOptions.version)) { - commandName = 'version'; - commandMetadata = findCommand(commandMap, commandName); - } else if (!commandMetadata && rawOptions.help) { - commandName = 'help'; - commandMetadata = findCommand(commandMap, commandName); + if (!arg.startsWith('-')) { + commandName = arg; + args.splice(i, 1); + break; + } } - if (!commandMetadata) { - if (!commandName) { - logger.error(tags.stripIndent` - We could not find a command from the arguments and the help 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. - `); + let description: CommandDescription | null = null; - return 1; + // if no commands were found, use `help`. + if (!commandName) { + if (args.length === 1 && args[0] === '--version') { + commandName = 'version'; } else { - const commandsDistance = {} as { [name: string]: number }; - const allCommands = Object.keys(commandMap).sort((a, b) => { - if (!(a in commandsDistance)) { - commandsDistance[a] = levenshtein(a, commandName); - } - if (!(b in commandsDistance)) { - commandsDistance[b] = levenshtein(b, commandName); - } - - return commandsDistance[a] - commandsDistance[b]; - }); + commandName = 'help'; + } + if (!(commandName in commands)) { logger.error(tags.stripIndent` - The specified command ("${commandName}") is invalid. For a list of available options, - run "ng help". - Did you mean "${allCommands[0]}"? - `); + 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; } } - const command = await createCommand(commandMetadata, context, logger); - const metadataOptions = await convertSchemaToOptions(commandMetadata.text); - if (command === null) { - logger.error(tags.stripIndent`Command (${commandName}) failed to instantiate.`); - - return 1; - } - // Add the options from the metadata to the command object. - command.addOptions(metadataOptions); - let options = parseOptions(args, metadataOptions); - args = await command.initializeRaw(args); - - const optionsCopy = deepCopy(options); - await processRegistry(optionsCopy, commandMetadata); - await command.initialize(optionsCopy); - - // Reparse options after initializing the command. - options = parseOptions(args, command.options); - - if (commandName === 'help') { - options.commandInfo = getAllCommandInfo(commandMap); - } - - if (options.help) { - command.printHelp(commandName, commandMetadata.rawData.description, options); - - return; + if (commandName in commands) { + description = await loadCommandDescription(commandName, commands[commandName], registry); } else { - const commandScope = mapCommandScope(commandMetadata.rawData.$scope); - if (commandScope !== undefined && commandScope !== CommandScope.everywhere) { - if (commandScope !== executionScope) { - let errorMessage; - if (commandScope === CommandScope.inProject) { - errorMessage = `This command can only be run inside of a CLI project.`; - } else { - errorMessage = `This command can not be run inside of a CLI project.`; - } - logger.fatal(errorMessage); - - return 1; - } - if (commandScope === CommandScope.inProject) { - if (!context.project.configFile) { - logger.fatal('Invalid project: missing workspace file.'); - + 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; } + }); + } - if (['.angular-cli.json', 'angular-cli.json'].includes(context.project.configFile)) { - // -------------------------------------------------------------------------------- - // If changing this message, please update the same message in - // `packages/@angular/cli/bin/ng-update-message.js` - const message = tags.stripIndent` - The Angular CLI configuration format has been changed, and your existing configuration - can be updated automatically by running the following command: - - ng update @angular/cli - `; - - logger.warn(message); + for (const name of commandNames) { + const aliasDesc = await loadCommandDescription(name, commands[name], registry); + const aliases = aliasDesc.aliases; - return 1; - } + if (aliases && aliases.some(alias => alias === commandName)) { + commandName = name; + description = aliasDesc; + break; } } } - delete options.h; - delete options.help; - await processRegistry(options, commandMetadata); - - const isValid = await command.validate(options); - if (!isValid) { - logger.fatal(`Validation error. Invalid command options.`); - - return 1; - } - - return command.run(options); -} - -async function processRegistry( - options: {_: (string | boolean | number)[]}, commandMetadata: CommandLocation) { - const rawArgs = options._; - const registry = new schema.CoreSchemaRegistry([]); - registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { - if ('index' in schema) { - return rawArgs[Number(schema['index'])]; - } else { - return rawArgs; - } - }); - const jsonSchema = parseSchema(commandMetadata.text); - if (jsonSchema === null) { - throw new Error(''); - } - await registry.compile(jsonSchema).pipe( - concatMap(validator => validator(options)), concatMap(validatorResult => { - if (validatorResult.success) { - return of(options); - } else { - return throwError(new schema.SchemaValidationException(validatorResult.errors)); + 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); } - })).toPromise(); -} - -export function parseOptions(args: string[], optionsAndArguments: Option[]) { - const parser = yargsParser; - - // filter out arguments - const options = optionsAndArguments - .filter(opt => { - let isOption = true; - if (opt.$default !== undefined && opt.$default.$source === 'argv') { - isOption = false; + if (!(b in commandsDistance)) { + commandsDistance[b] = strings.levenshtein(b, name); } - return isOption; + return commandsDistance[a] - commandsDistance[b]; }); - const aliases: { [key: string]: string[]; } = options - .reduce((aliases: { [key: string]: string; }, opt) => { - if (!opt || !opt.aliases || opt.aliases.length === 0) { - return aliases; - } + logger.error(tags.stripIndent` + The specified command ("${commandName}") is invalid. For a list of available options, + run "ng help". - aliases[opt.name] = (opt.aliases || []) - .filter(a => a.length === 1)[0]; + Did you mean "${allCommands[0]}"? + `); - return aliases; - }, {}); - - const booleans = options - .filter(o => o.type && o.type === 'boolean') - .map(o => o.name); - - const defaults = options - .filter(o => o.default !== undefined || booleans.indexOf(o.name) !== -1) - .reduce((defaults: {[key: string]: string | number | boolean | undefined }, opt: Option) => { - defaults[opt.name] = opt.default; - - return defaults; - }, {}); - - const strings = options - .filter(o => o.type === 'string') - .map(o => o.name); - - const numbers = options - .filter(o => o.type === 'number') - .map(o => o.name); - - - aliases.help = ['h']; - booleans.push('help'); - - const yargsOptions = { - alias: aliases, - boolean: booleans, - default: defaults, - string: strings, - number: numbers, - }; - - const parsedOptions = parser(args, yargsOptions); + return 1; + } - // Remove aliases. - options - .reduce((allAliases, option) => { - if (!option || !option.aliases || option.aliases.length === 0) { - return allAliases; + 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 allAliases.concat([...option.aliases]); - }, [] as string[]) - .forEach((alias: string) => { - delete parsedOptions[alias]; + return map; }); - // Remove undefined booleans - booleans - .filter(b => parsedOptions[b] === undefined) - .map(b => coreStrings.camelize(b)) - .forEach(b => delete parsedOptions[b]); + const analytics = options.analytics || await _createAnalytics(); + const context = { workspace, analytics }; + const command = new description.impl(context, description, logger); - // remove options with dashes. - Object.keys(parsedOptions) - .filter(key => key.indexOf('-') !== -1) - .forEach(key => delete parsedOptions[key]); + // Flush on an interval (if the event loop is waiting). + let analyticsFlushPromise = Promise.resolve(); + setInterval(() => { + analyticsFlushPromise = analyticsFlushPromise.then(() => analytics.flush()); + }, 1000); - // remove the command name - parsedOptions._ = parsedOptions._.slice(1); + const result = await command.validateAndRun(parsedOptions); - return parsedOptions; -} + // Flush one last time. + await analyticsFlushPromise.then(() => analytics.flush()); -// Find a command. -function findCommand(map: CommandMap, name: string): CommandLocation | null { - // let Cmd: CommandConstructor = map[name]; - let commandName = name; - - if (!map[commandName]) { - // find command via aliases - commandName = Object.keys(map) - .filter(key => { - // get aliases for the key - const metadataText = readFileSync(map[key]).toString('utf-8'); - const metadata = JSON.parse(metadataText); - const aliases = metadata['$aliases']; - if (!aliases) { - return false; - } - const foundAlias = aliases.filter((alias: string) => alias === name); + 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 foundAlias.length > 0; - })[0]; - } - - const metadataPath = map[commandName]; - - if (!metadataPath) { - return null; - } - const metadataText = readFileSync(metadataPath).toString('utf-8'); - - const metadata = parseJson(metadataText) as any; - - return { - path: metadataPath, - text: metadataText, - rawData: metadata, - }; -} - -// Create an instance of a command. -async function createCommand(metadata: CommandLocation, - context: CommandContext, - logger: logging.Logger): Promise { - const schema = parseSchema(metadata.text); - if (schema === null) { - return null; - } - const implPath = schema.$impl; - if (typeof implPath !== 'string') { - throw new Error('Implementation path is incorrect'); - } - - const implRef = new ExportStringRef(implPath, dirname(metadata.path)); - - const ctor = implRef.ref as CommandConstructor; - - return new ctor(context, logger); -} - -function mapCommandScope(scope: 'in' | 'out' | undefined): CommandScope { - let commandScope = CommandScope.everywhere; - switch (scope) { - case 'in': - commandScope = CommandScope.inProject; - break; - case 'out': - commandScope = CommandScope.outsideProject; - break; + return 1; + } else { + throw e; + } } - - return commandScope; -} - -interface CommandInfo { - name: string; - description: string; - aliases: string[]; - hidden: boolean; -} -function getAllCommandInfo(map: CommandMap): CommandInfo[] { - return Object.keys(map) - .map(name => { - return { - name: name, - metadata: findCommand(map, name), - }; - }) - .map(info => { - if (info.metadata === null) { - return null; - } - - return { - name: info.name, - description: info.metadata.rawData.description, - aliases: info.metadata.rawData.$aliases || [], - hidden: info.metadata.rawData.$hidden || false, - }; - }) - .filter(info => info !== null) as CommandInfo[]; } diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 511abc6a813d..0e783fb18318 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -7,151 +7,186 @@ */ // tslint:disable:no-global-tslint-disable no-any -import { JsonValue, logging, terminal } from '@angular-devkit/core'; - -export interface CommandConstructor { - new(context: CommandContext, logger: logging.Logger): Command; - readonly name: string; - aliases: string[]; - scope: CommandScope; -} - -export enum CommandScope { - everywhere, - inProject, - outsideProject, -} - -export enum ArgumentStrategy { - MapToOptions, - Nothing, +import { analytics, logging, strings, tags } from '@angular-devkit/core'; +import * as path from 'path'; +import { colors } from '../utilities/color'; +import { getWorkspace } from '../utilities/config'; +import { + Arguments, + CommandContext, + CommandDescription, + CommandDescriptionMap, + CommandScope, + CommandWorkspace, + Option, + SubCommandDescription, +} from './interface'; + +export interface BaseCommandOptions { + help?: boolean | string; } -export abstract class Command { - protected _rawArgs: string[]; +export abstract class Command { public allowMissingWorkspace = false; + public workspace: CommandWorkspace; + public analytics: analytics.Analytics; - constructor(context: CommandContext, logger: logging.Logger) { - this.logger = logger; - if (context) { - this.project = context.project; - } + protected static commandMap: () => Promise; + static setCommandMap(map: () => Promise) { + this.commandMap = map; } - public addOptions(options: Option[]) { - this.options = (this.options || []).concat(options); + constructor( + context: CommandContext, + public readonly description: CommandDescription, + protected readonly logger: logging.Logger, + ) { + this.workspace = context.workspace; + this.analytics = context.analytics || new analytics.NoopAnalytics(); } - async initializeRaw(args: string[]): Promise { - this._rawArgs = args; - - return args; - } - async initialize(_options: any): Promise { + async initialize(options: T & Arguments): Promise { return; } - validate(_options: T): boolean | Promise { - return true; + async printHelp(options: T & Arguments): Promise { + await this.printHelpUsage(); + await this.printHelpOptions(); + + return 0; } - printHelp(commandName: string, description: string, options: any): void { - if (description) { - this.logger.info(description); - } - this.printHelpUsage(commandName, this.options); - this.printHelpOptions(this.options); + async printJsonHelp(_options: T & Arguments): Promise { + this.logger.info(JSON.stringify(this.description)); + + return 0; } - private _getArguments(options: Option[]) { - function _getArgIndex(def: OptionSmartDefault | undefined): number { - if (def === undefined || def.$source !== 'argv' || typeof def.index !== 'number') { - // If there's no proper order, this argument is wonky. We will show it at the end only - // (after all other arguments). - return Infinity; - } + protected async printHelpUsage() { + this.logger.info(this.description.description); - return def.index; - } + 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); - return options - .filter(opt => this.isArgument(opt)) - .sort((a, b) => _getArgIndex(a.$default) - _getArgIndex(b.$default)); - } + const argDisplay = args && args.length > 0 ? ' ' + args.map(a => `<${a.name}>`).join(' ') : ''; + const optionsDisplay = opts && opts.length > 0 ? ` [options]` : ``; - protected printHelpUsage(name: string, options: Option[]) { - const args = this._getArguments(options); - const opts = options.filter(opt => !this.isArgument(opt)); - 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 isArgument(option: Option) { - let isArg = false; - if (option.$default !== undefined && option.$default.$source === 'argv') { - isArg = true; - } + protected async printHelpSubcommand(subcommand: SubCommandDescription) { + this.logger.info(subcommand.description); - return isArg; + await this.printHelpOptions(subcommand.options); } - protected printHelpOptions(options: Option[]) { - if (!options) { - return; - } - const args = options.filter(opt => this.isArgument(opt)); - const opts = options.filter(opt => !this.isArgument(opt)); + 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(` ${terminal.cyan(o.name)}`); - this.logger.info(` ${o.description}`); + this.logger.info(` ${colors.cyan(o.name)}`); + if (o.description) { + this.logger.info(formatDescription(o.description)); + } }); } - if (this.options.length > 0) { + 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 >= b.name ? 1 : -1) + .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(` ${terminal.cyan('--' + o.name)} ${aliases}`); - this.logger.info(` ${o.description}`); + 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)); + } }); } } - abstract run(options: T): number | void | Promise; - public options: Option[]; - public additionalSchemas: string[] = []; - protected readonly logger: logging.Logger; - protected readonly project: any; -} + async validateScope(scope?: CommandScope): Promise { + switch (scope === undefined ? this.description.scope : scope) { + case CommandScope.OutProject: + if (this.workspace.configFile) { + this.logger.fatal(tags.oneLine` + The ${this.description.name} command requires to be run outside of a project, but a + project definition was found at "${path.join( + this.workspace.root, + this.workspace.configFile, + )}". + `); + throw 1; + } + break; + case CommandScope.InProject: + if (!this.workspace.configFile || getWorkspace('local') === null) { + this.logger.fatal(tags.oneLine` + The ${this.description.name} command requires to be run in an Angular project, but a + project definition could not be found. + `); + throw 1; + } + break; + case CommandScope.Everywhere: + // Can't miss this. + break; + } + } -export interface CommandContext { - project: any; -} + async reportAnalytics( + paths: string[], + options: T & Arguments, + dimensions: (boolean | number | string)[] = [], + metrics: (boolean | number | string)[] = [], + ): Promise { + for (const option of this.description.options) { + const ua = option.userAnalytics; + const v = options[option.name]; + + if (v !== undefined && !Array.isArray(v) && ua) { + dimensions[ua] = v; + } + } -export interface Option { - name: string; - description: string; - type: string; - default?: string | number | boolean; - required?: boolean; - aliases?: string[]; - format?: string; - hidden?: boolean; - $default?: OptionSmartDefault; -} + this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics }); + } + + abstract async run(options: T & Arguments): Promise; + + async validateAndRun(options: T & Arguments): Promise { + if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { + await this.validateScope(); + } + await this.initialize(options); -export interface OptionSmartDefault { - $source: string; - [key: string]: JsonValue; + if (options.help === true) { + return this.printHelp(options); + } else if (options.help === 'json' || options.help === 'JSON') { + return this.printJsonHelp(options); + } else { + const startTime = +new Date(); + await this.reportAnalytics([this.description.name], options); + const result = await this.run(options); + const endTime = +new Date(); + + this.analytics.timing(this.description.name, 'duration', endTime - startTime); + + return result; + } + } } diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts new file mode 100644 index 000000000000..17292c95269b --- /dev/null +++ b/packages/angular/cli/models/interface.ts @@ -0,0 +1,242 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { analytics, json, logging } from '@angular-devkit/core'; + +/** + * Value type of arguments. + */ +export type Value = number | string | boolean | (number | string | boolean)[]; + +/** + * An object representing parsed arguments from the command line. + */ +export interface Arguments { + [argName: string]: Value | undefined; + + /** + * Extra arguments that were not parsed. Will be omitted if all arguments were parsed. + */ + '--'?: string[]; +} + +/** + * The base interface for Command, understood by the command runner. + */ +export interface CommandInterface { + printHelp(options: T): Promise; + printJsonHelp(options: T): Promise; + validateAndRun(options: T): Promise; +} + +/** + * Command constructor. + */ +export interface CommandConstructor { + new( + context: CommandContext, + description: CommandDescription, + logger: logging.Logger, + ): CommandInterface; +} + +/** + * A CLI workspace information. + */ +export interface CommandWorkspace { + root: string; + configFile?: string; +} + +/** + * A command runner context. + */ +export interface CommandContext { + workspace: CommandWorkspace; + + // This feel is optional for backward compatibility. + analytics?: analytics.Analytics; +} + +/** + * Value types of an Option. + */ +export enum OptionType { + Any = 'any', + Array = 'array', + Boolean = 'boolean', + Number = 'number', + String = 'string', +} + +/** + * An option description. This is exposed when using `ng --help=json`. + */ +export interface Option { + /** + * The name of the option. + */ + name: string; + + /** + * A short description of the option. + */ + description: string; + + /** + * The type of option value. If multiple types exist, this type will be the first one, and the + * types array will contain all types accepted. + */ + type: OptionType; + + /** + * {@see type} + */ + types?: OptionType[]; + + /** + * If this field is set, only values contained in this field are valid. This array can be mixed + * types (strings, numbers, boolean). For example, if this field is "enum: ['hello', true]", + * then "type" will be either string or boolean, types will be at least both, and the values + * accepted will only be either 'hello' or true (not false or any other string). + * This mean that prefixing with `no-` will not work on this field. + */ + enum?: Value[]; + + /** + * If this option maps to a subcommand in the parent command, will contain all the subcommands + * supported. There is a maximum of 1 subcommand Option per command, and the type of this + * option will always be "string" (no other types). The value of this option will map into + * this map and return the extra information. + */ + subcommands?: { + [name: string]: SubCommandDescription; + }; + + /** + * Aliases supported by this option. + */ + aliases: string[]; + + /** + * Whether this option is required or not. + */ + required?: boolean; + + /** + * Format field of this option. + */ + format?: string; + + /** + * Whether this option should be hidden from the help output. It will still show up in JSON help. + */ + hidden?: boolean; + + /** + * Default value of this option. + */ + default?: string | number | boolean; + + /** + * If this option can be used as an argument, the position of the argument. Otherwise omitted. + */ + positional?: number; + + /** + * Deprecation. If this flag is not false a warning will be shown on the console. Either `true` + * or a string to show the user as a notice. + */ + deprecated?: boolean | string; + + /** + * Smart default object. + */ + $default?: OptionSmartDefault; + + /** + * Whether or not to report this option to the Angular Team, and which custom field to use. + * If this is falsey, do not report this option. + */ + userAnalytics?: number; +} + +/** + * Scope of the command. + */ +export enum CommandScope { + InProject = 'in', + OutProject = 'out', + Everywhere = 'all', + + Default = InProject, +} + +/** + * A description of a command and its options. + */ +export interface SubCommandDescription { + /** + * The name of the subcommand. + */ + name: string; + + /** + * Short description (1-2 lines) of this sub command. + */ + description: string; + + /** + * A long description of the sub command, in Markdown format. + */ + longDescription?: string; + + /** + * Additional notes about usage of this sub command, in Markdown format. + */ + usageNotes?: string; + + /** + * List of all supported options. + */ + options: Option[]; + + /** + * Aliases supported for this sub command. + */ + aliases: string[]; +} + +/** + * A description of a command, its metadata. + */ +export interface CommandDescription extends SubCommandDescription { + /** + * Scope of the command, whether it can be executed in a project, outside of a project or + * anywhere. + */ + scope: CommandScope; + + /** + * Whether this command should be hidden from a list of all commands. + */ + hidden: boolean; + + /** + * The constructor of the command, which should be extending the abstract Command<> class. + */ + impl: CommandConstructor; +} + +export interface OptionSmartDefault { + $source: string; + [key: string]: json.JsonValue; +} + +export interface CommandDescriptionMap { + [key: string]: CommandDescription; +} diff --git a/packages/angular/cli/models/json-schema.ts b/packages/angular/cli/models/json-schema.ts deleted file mode 100644 index 1dcb8aee16d3..000000000000 --- a/packages/angular/cli/models/json-schema.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { JsonObject, isJsonObject, parseJson } from '@angular-devkit/core'; -import * as jsonSchemaTraverse from 'json-schema-traverse'; -import { Option, OptionSmartDefault } from './command'; - -export async function convertSchemaToOptions(schema: string): Promise { - const options = await getOptions(schema); - - return options; -} - -function getOptions(schemaText: string, onlyRootProperties = true): Promise { - // TODO: Use devkit core's visitJsonSchema - return new Promise((resolve) => { - const fullSchema = parseJson(schemaText); - if (!isJsonObject(fullSchema)) { - return Promise.resolve([]); - } - const traverseOptions = {}; - const options: Option[] = []; - function postCallback(schema: JsonObject, - jsonPointer: string, - _rootSchema: string, - _parentJsonPointer: string, - parentKeyword: string, - _parentSchema: string, - property: string) { - if (parentKeyword === 'properties') { - let includeOption = true; - if (onlyRootProperties && isPropertyNested(jsonPointer)) { - includeOption = false; - } - const description = typeof schema.description == 'string' ? schema.description : ''; - const type = typeof schema.type == 'string' ? schema.type : ''; - let defaultValue: string | number | boolean | undefined = undefined; - if (schema.default !== null) { - if (typeof schema.default !== 'object') { - defaultValue = schema.default; - } - } - let $default: OptionSmartDefault | undefined = undefined; - if (schema.$default !== null && isJsonObject(schema.$default)) { - $default = schema.$default as OptionSmartDefault; - } - let required = false; - if (typeof schema.required === 'boolean') { - required = schema.required; - } - let aliases: string[] | undefined = undefined; - if (typeof schema.aliases === 'object' && Array.isArray(schema.aliases)) { - aliases = schema.aliases as string[]; - } - let format: string | undefined = undefined; - if (typeof schema.format === 'string') { - format = schema.format; - } - let hidden = false; - if (typeof schema.hidden === 'boolean') { - hidden = schema.hidden; - } - - const option: Option = { - name: property, - // ...schema, - - description, - type, - default: defaultValue, - $default, - required, - aliases, - format, - hidden, - }; - - if (includeOption) { - options.push(option); - } - } else if (schema === fullSchema) { - resolve(options); - } - } - - const callbacks = { post: postCallback }; - - jsonSchemaTraverse(fullSchema, traverseOptions, callbacks); - }); -} - -function isPropertyNested(jsonPath: string): boolean { - return jsonPath.split('/') - .filter(part => part == 'properties' || part == 'items') - .length > 1; -} - -export function parseSchema(schema: string): JsonObject | null { - const parsedSchema = parseJson(schema); - if (parsedSchema === null || !isJsonObject(parsedSchema)) { - return null; - } - - return parsedSchema; -} diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts new file mode 100644 index 000000000000..2d5dbf7495e6 --- /dev/null +++ b/packages/angular/cli/models/parser.ts @@ -0,0 +1,407 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +import { BaseException, logging, strings } from '@angular-devkit/core'; +import { Arguments, Option, OptionType, Value } from './interface'; + + +export class ParseArgumentException extends BaseException { + constructor( + public readonly comments: string[], + public readonly parsed: Arguments, + public readonly ignored: string[], + ) { + super(`One or more errors occurred while parsing arguments:\n ${comments.join('\n ')}`); + } +} + + +function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined { + switch (type) { + case OptionType.Any: + if (Array.isArray(v)) { + return v.concat(str || ''); + } + + return _coerceType(str, OptionType.Boolean, v) !== undefined + ? _coerceType(str, OptionType.Boolean, v) + : _coerceType(str, OptionType.Number, v) !== undefined + ? _coerceType(str, OptionType.Number, v) + : _coerceType(str, OptionType.String, v); + + case OptionType.String: + return str || ''; + + case OptionType.Boolean: + switch (str) { + case 'false': + return false; + + case undefined: + case '': + case 'true': + return true; + + default: + return undefined; + } + + case OptionType.Number: + if (str === undefined) { + return 0; + } else if (str === '') { + return undefined; + } else if (Number.isFinite(+str)) { + return +str; + } else { + return undefined; + } + + case OptionType.Array: + return Array.isArray(v) + ? v.concat(str || '') + : v === undefined + ? [str || ''] + : [v + '', str || '']; + + default: + return undefined; + } +} + +function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { + if (!o) { + return _coerceType(str, OptionType.Any, v); + } else { + const types = o.types || [o.type]; + + // Try all the types one by one and pick the first one that returns a value contained in the + // enum. If there's no enum, just return the first one that matches. + for (const type of types) { + const maybeResult = _coerceType(str, type, v); + if (maybeResult !== undefined) { + if (!o.enum || o.enum.includes(maybeResult)) { + return maybeResult; + } + } + } + + return undefined; + } +} + + +function _getOptionFromName(name: string, options: Option[]): Option | undefined { + const camelName = /(-|_)/.test(name) + ? strings.camelize(name) + : name; + + for (const option of options) { + if (option.name === name || option.name === camelName) { + return option; + } + + if (option.aliases.some(x => x === name || x === camelName)) { + return option; + } + } + + return undefined; +} + +function _removeLeadingDashes(key: string): string { + const from = key.startsWith('--') ? 2 : key.startsWith('-') ? 1 : 0; + + return key.substr(from); +} + +function _assignOption( + arg: string, + nextArg: string | undefined, + { options, parsedOptions, leftovers, ignored, errors, warnings }: { + options: Option[], + parsedOptions: Arguments, + positionals: string[], + leftovers: string[], + ignored: string[], + errors: string[], + warnings: string[], + }, +) { + const from = arg.startsWith('--') ? 2 : 1; + let consumedNextArg = false; + let key = arg.substr(from); + let option: Option | null = null; + let value: string | undefined = ''; + const i = arg.indexOf('='); + + // If flag is --no-abc AND there's no equal sign. + if (i == -1) { + if (key.startsWith('no')) { + // Only use this key if the option matching the rest is a boolean. + const from = key.startsWith('no-') ? 3 : 2; + const maybeOption = _getOptionFromName(strings.camelize(key.substr(from)), options); + if (maybeOption && maybeOption.type == 'boolean') { + value = 'false'; + option = maybeOption; + } + } + + if (option === null) { + // Set it to true if it's a boolean and the next argument doesn't match true/false. + const maybeOption = _getOptionFromName(key, options); + if (maybeOption) { + value = nextArg; + let shouldShift = true; + + if (value && value.startsWith('-')) { + // Verify if not having a value results in a correct parse, if so don't shift. + if (_coerce(undefined, maybeOption) !== undefined) { + shouldShift = false; + } + } + + // Only absorb it if it leads to a better value. + if (shouldShift && _coerce(value, maybeOption) !== undefined) { + consumedNextArg = true; + } else { + value = ''; + } + option = maybeOption; + } + } + } else { + key = arg.substring(0, i); + option = _getOptionFromName(_removeLeadingDashes(key), options) || null; + if (option) { + value = arg.substring(i + 1); + } + } + + if (option === null) { + if (nextArg && !nextArg.startsWith('-')) { + leftovers.push(arg, nextArg); + consumedNextArg = true; + } else { + leftovers.push(arg); + } + } else { + const v = _coerce(value, option, parsedOptions[option.name]); + if (v !== undefined) { + if (parsedOptions[option.name] !== v) { + if (parsedOptions[option.name] !== undefined) { + warnings.push( + `Option ${JSON.stringify(option.name)} was already specified with value ` + + `${JSON.stringify(parsedOptions[option.name])}. The new value ${JSON.stringify(v)} ` + + `will override it.`, + ); + } + + parsedOptions[option.name] = v; + + if (option.deprecated !== undefined && option.deprecated !== false) { + warnings.push(`Option ${JSON.stringify(option.name)} is deprecated${ + typeof option.deprecated == 'string' ? ': ' + option.deprecated : '.'}`); + } + } + } else { + let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`; + if (option.enum) { + error += ` Valid values are: ${option.enum.map(x => JSON.stringify(x)).join(', ')}.`; + } else { + error += `Valid type(s) is: ${(option.types || [option.type]).join(', ')}`; + } + + errors.push(error); + ignored.push(arg); + } + } + + return consumedNextArg; +} + + +/** + * Parse the arguments in a consistent way, but without having any option definition. This tries + * to assess what the user wants in a free form. For example, using `--name=false` will set the + * name properties to a boolean type. + * This should only be used when there's no schema available or if a schema is "true" (anything is + * valid). + * + * @param args Argument list to parse. + * @returns An object that contains a property per flags from the args. + */ +export function parseFreeFormArguments(args: string[]): Arguments { + const parsedOptions: Arguments = {}; + const leftovers = []; + + for (let arg = args.shift(); arg !== undefined; arg = args.shift()) { + if (arg == '--') { + leftovers.push(...args); + break; + } + + if (arg.startsWith('--')) { + const eqSign = arg.indexOf('='); + let name: string; + let value: string | undefined; + if (eqSign !== -1) { + name = arg.substring(2, eqSign); + value = arg.substring(eqSign + 1); + } else { + name = arg.substr(2); + value = args.shift(); + } + + const v = _coerce(value, null, parsedOptions[name]); + if (v !== undefined) { + parsedOptions[name] = v; + } + } else if (arg.startsWith('-')) { + arg.split('').forEach(x => parsedOptions[x] = true); + } else { + leftovers.push(arg); + } + } + + 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 new file mode 100644 index 000000000000..86b9cba71211 --- /dev/null +++ b/packages/angular/cli/models/parser_spec.ts @@ -0,0 +1,241 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +// tslint:disable:no-global-tslint-disable no-big-function +import { logging } from '@angular-devkit/core'; +import { Arguments, Option, OptionType } from './interface'; +import { ParseArgumentException, parseArguments } from './parser'; + +describe('parseArguments', () => { + const options: Option[] = [ + { name: 'bool', aliases: [ 'b' ], type: OptionType.Boolean, description: '' }, + { name: 'num', aliases: [ 'n' ], type: OptionType.Number, description: '' }, + { name: 'str', aliases: [ 's' ], type: OptionType.String, description: '' }, + { name: 'strUpper', aliases: [ 'S' ], type: OptionType.String, description: '' }, + { name: 'helloWorld', aliases: [], type: OptionType.String, description: '' }, + { name: 'helloBool', aliases: [], type: OptionType.Boolean, description: '' }, + { name: 'arr', aliases: [ 'a' ], type: OptionType.Array, description: '' }, + { name: 'p1', positional: 0, aliases: [], type: OptionType.String, description: '' }, + { name: 'p2', positional: 1, aliases: [], type: OptionType.String, description: '' }, + { name: 'p3', positional: 2, aliases: [], type: OptionType.Number, description: '' }, + { name: 't1', aliases: [], type: OptionType.Boolean, + types: [OptionType.Boolean, OptionType.String], description: '' }, + { name: 't2', aliases: [], type: OptionType.Boolean, + types: [OptionType.Boolean, OptionType.Number], description: '' }, + { name: 't3', aliases: [], type: OptionType.Number, + types: [OptionType.Number, OptionType.Any], description: '' }, + { name: 'e1', aliases: [], type: OptionType.String, enum: ['hello', 'world'], description: '' }, + { name: 'e2', aliases: [], type: OptionType.String, enum: ['hello', ''], description: '' }, + { name: 'e3', aliases: [], type: OptionType.Boolean, + types: [OptionType.Boolean, OptionType.String], enum: ['json', true, false], + description: '' }, + ]; + + const tests: { [test: string]: Partial | ['!!!', Partial, string[]] } = { + '-S=b': { strUpper: 'b' }, + '--bool': { bool: true }, + '--bool=1': ['!!!', {}, ['--bool=1']], + '--bool ': { bool: true, p1: '' }, + '-- --bool=1': { '--': ['--bool=1'] }, + '--bool=yellow': ['!!!', {}, ['--bool=yellow']], + '--bool=true': { bool: true }, + '--bool=false': { bool: false }, + '--no-bool': { bool: false }, + '--no-bool=true': { '--': ['--no-bool=true'] }, + '--b=true': { bool: true }, + '--b=false': { bool: false }, + '--b true': { bool: true }, + '--b false': { bool: false }, + '--bool --num': { bool: true, num: 0 }, + '--bool --num=true': ['!!!', { bool: true }, ['--num=true']], + '-- --bool --num=true': { '--': ['--bool', '--num=true'] }, + '--bool=true --num': { bool: true, num: 0 }, + '--bool true --num': { bool: true, num: 0 }, + '--bool=false --num': { bool: false, num: 0 }, + '--bool false --num': { bool: false, num: 0 }, + '--str false --num': { str: 'false', num: 0 }, + '--str=false --num': { str: 'false', num: 0 }, + '--str=false --num1': { str: 'false', '--': ['--num1'] }, + '--str=false val1 --num1': { str: 'false', p1: 'val1', '--': ['--num1'] }, + '--str=false val1 val2': { str: 'false', p1: 'val1', p2: 'val2' }, + '--str=false val1 val2 --num1': { str: 'false', p1: 'val1', p2: 'val2', '--': ['--num1'] }, + '--str=false val1 --num1 val2': { str: 'false', p1: 'val1', '--': ['--num1', 'val2'] }, + '--bool --bool=false': { bool: false }, + '--bool --bool=false --bool': { bool: true }, + '--num=1 --num=2 --num=3': { num: 3 }, + '--str=1 --str=2 --str=3': { str: '3' }, + 'val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, + '--p1=val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, + '--p1=val1 --num=1 --p2=val2 val3': { num: 1, p1: 'val1', p2: 'val2', '--': ['val3'] }, + '--bool val1 --etc --num val2 --v': [ + '!!!', + { bool: true, p1: 'val1', p2: 'val2', '--': ['--etc', '--v'] }, + ['--num' ], + ], + '--bool val1 --etc --num=1 val2 --v': { bool: true, num: 1, p1: 'val1', p2: 'val2', + '--': ['--etc', '--v'] }, + '--arr=a d': { arr: ['a'], p1: 'd' }, + '--arr=a --arr=b --arr c d': { arr: ['a', 'b', 'c'], p1: 'd' }, + '--arr=1 --arr --arr c d': { arr: ['1', '', 'c'], p1: 'd' }, + '--arr=1 --arr --arr c d e': { arr: ['1', '', 'c'], p1: 'd', p2: 'e' }, + '--str=1': { str: '1' }, + '--str=': { str: '' }, + '--str ': { str: '' }, + '--str ': { str: '', p1: '' }, + '--str ': { str: '', p1: '', p2: '', '--': [''] }, + '--hello-world=1': { helloWorld: '1' }, + '--hello-bool': { helloBool: true }, + '--helloBool': { helloBool: true }, + '--no-helloBool': { helloBool: false }, + '--noHelloBool': { helloBool: false }, + '--noBool': { bool: false }, + '-b': { bool: true }, + '-b=true': { bool: true }, + '-sb': { bool: true, str: '' }, + '-s=b': { str: 'b' }, + '-bs': { bool: true, str: '' }, + '--t1=true': { t1: true }, + '--t1': { t1: true }, + '--t1 --num': { t1: true, num: 0 }, + '--no-t1': { t1: false }, + '--t1=yellow': { t1: 'yellow' }, + '--no-t1=true': { '--': ['--no-t1=true'] }, + '--t1=123': { t1: '123' }, + '--t2=true': { t2: true }, + '--t2': { t2: true }, + '--no-t2': { t2: false }, + '--t2=yellow': ['!!!', {}, ['--t2=yellow']], + '--no-t2=true': { '--': ['--no-t2=true'] }, + '--t2=123': { t2: 123 }, + '--t3=a': { t3: 'a' }, + '--t3': { t3: 0 }, + '--t3 true': { t3: true }, + '--e1 hello': { e1: 'hello' }, + '--e1=hello': { e1: 'hello' }, + '--e1 yellow': ['!!!', { p1: 'yellow' }, ['--e1']], + '--e1=yellow': ['!!!', {}, ['--e1=yellow']], + '--e1': ['!!!', {}, ['--e1']], + '--e1 true': ['!!!', { p1: 'true' }, ['--e1']], + '--e1=true': ['!!!', {}, ['--e1=true']], + '--e2 hello': { e2: 'hello' }, + '--e2=hello': { e2: 'hello' }, + '--e2 yellow': { p1: 'yellow', e2: '' }, + '--e2=yellow': ['!!!', {}, ['--e2=yellow']], + '--e2': { e2: '' }, + '--e2 true': { p1: 'true', e2: '' }, + '--e2=true': ['!!!', {}, ['--e2=true']], + '--e3 json': { e3: 'json' }, + '--e3=json': { e3: 'json' }, + '--e3 yellow': { p1: 'yellow', e3: true }, + '--e3=yellow': ['!!!', {}, ['--e3=yellow']], + '--e3': { e3: true }, + '--e3 true': { e3: true }, + '--e3=true': { e3: true }, + 'a b c 1': { p1: 'a', p2: 'b', '--': ['c', '1'] }, + + '-p=1 -c=prod': {'--': ['-p=1', '-c=prod'] }, + '--p --c': {'--': ['--p', '--c'] }, + '--p=123': {'--': ['--p=123'] }, + '--p -c': {'--': ['--p', '-c'] }, + '-p --c': {'--': ['-p', '--c'] }, + '-p --c 123': {'--': ['-p', '--c', '123'] }, + '--c 123 -p': {'--': ['--c', '123', '-p'] }, + }; + + Object.entries(tests).forEach(([str, expected]) => { + it(`works for ${str}`, () => { + try { + const originalArgs = str.split(' '); + const args = originalArgs.slice(); + + const actual = parseArguments(args, options); + + expect(Array.isArray(expected)).toBe(false); + expect(actual).toEqual(expected as Arguments); + expect(args).toEqual(originalArgs); + } catch (e) { + if (!(e instanceof ParseArgumentException)) { + throw e; + } + + // The expected values are an array. + expect(Array.isArray(expected)).toBe(true); + expect(e.parsed).toEqual(expected[1] as Arguments); + expect(e.ignored).toEqual(expected[2] as string[]); + } + }); + }); + + it('handles deprecation', () => { + const options = [ + { name: 'bool', aliases: [], type: OptionType.Boolean, description: '' }, + { name: 'depr', aliases: [], type: OptionType.Boolean, description: '', deprecated: true }, + { name: 'deprM', aliases: [], type: OptionType.Boolean, description: '', deprecated: 'ABCD' }, + ]; + + const logger = new logging.Logger(''); + const messages: string[] = []; + + logger.subscribe(entry => messages.push(entry.message)); + + let result = parseArguments(['--bool'], options, logger); + expect(result).toEqual({ bool: true }); + expect(messages).toEqual([]); + + result = parseArguments(['--depr'], options, logger); + expect(result).toEqual({ depr: true }); + expect(messages.length).toEqual(1); + expect(messages[0]).toMatch(/\bdepr\b/); + messages.shift(); + + result = parseArguments(['--depr', '--bool'], options, logger); + expect(result).toEqual({ depr: true, bool: true }); + expect(messages.length).toEqual(1); + expect(messages[0]).toMatch(/\bdepr\b/); + messages.shift(); + + result = parseArguments(['--depr', '--bool', '--deprM'], options, logger); + expect(result).toEqual({ depr: true, deprM: true, bool: true }); + expect(messages.length).toEqual(2); + expect(messages[0]).toMatch(/\bdepr\b/); + expect(messages[1]).toMatch(/\bdeprM\b/); + expect(messages[1]).toMatch(/\bABCD\b/); + messages.shift(); + }); + + it('handles a flag being added multiple times', () => { + const options = [ + { name: 'bool', aliases: [], type: OptionType.Boolean, description: '' }, + ]; + + const logger = new logging.Logger(''); + const messages: string[] = []; + + logger.subscribe(entry => messages.push(entry.message)); + + let result = parseArguments(['--bool'], options, logger); + expect(result).toEqual({ bool: true }); + expect(messages).toEqual([]); + + result = parseArguments(['--bool', '--bool'], options, logger); + expect(result).toEqual({ bool: true }); + expect(messages).toEqual([]); + + result = parseArguments(['--bool', '--bool=false'], options, logger); + expect(result).toEqual({ bool: false }); + expect(messages.length).toEqual(1); + expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); + messages.shift(); + + result = parseArguments(['--bool', '--bool=false', '--bool=false'], options, logger); + expect(result).toEqual({ bool: false }); + expect(messages.length).toEqual(1); + expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); + messages.shift(); + }); +}); diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 81594ac651d2..7cd74c07caf6 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -5,70 +5,62 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { - JsonObject, - experimental, + json, logging, normalize, schema, strings, tags, - terminal, virtualFs, + workspaces, } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { - Collection, DryRunEvent, - Engine, - Schematic, - SchematicEngine, UnsuccessfulWorkflowExecution, formats, workflow, } from '@angular-devkit/schematics'; import { - FileSystemCollectionDesc, - FileSystemEngineHostBase, - FileSystemSchematicDesc, - NodeModulesEngineHost, + FileSystemCollection, + FileSystemEngine, + FileSystemSchematic, + FileSystemSchematicDescription, NodeWorkflow, validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; -import { take } from 'rxjs/operators'; -import { WorkspaceLoader } from '../models/workspace-loader'; +import * as inquirer from 'inquirer'; +import * as systemPath from 'path'; +import { colors } from '../utilities/color'; import { - getDefaultSchematicCollection, - getPackageManager, + getProjectByCwd, getSchematicDefaults, + getWorkspace, + getWorkspaceRaw, } from '../utilities/config'; -import { ArgumentStrategy, Command, CommandContext, Option } from './command'; - -export interface CoreSchematicOptions { - dryRun: boolean; - force: boolean; -} - -export interface RunSchematicOptions { - collectionName: string; - schematicName: string; - schematicOptions: any; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; +import { getPackageManager } from '../utilities/package-manager'; +import { isTTY } from '../utilities/tty'; +import { isPackageNameSafeForAnalytics } from './analytics'; +import { BaseCommandOptions, Command } from './command'; +import { Arguments, CommandContext, CommandDescription, Option } from './interface'; +import { parseArguments, parseFreeFormArguments } from './parser'; + +export interface BaseSchematicSchema { debug?: boolean; - dryRun: boolean; - force: boolean; - showNothingDone?: boolean; + dryRun?: boolean; + force?: boolean; + interactive?: boolean; + defaults?: boolean; } -export interface GetOptionsOptions { +export interface RunSchematicOptions extends BaseSchematicSchema { collectionName: string; schematicName: string; -} - -export interface GetOptionsResult { - options: Option[]; - arguments: Option[]; + additionalOptions?: { [key: string]: {} }; + schematicOptions?: string[]; + showNothingDone?: boolean; } export class UnknownCollectionError extends Error { @@ -77,58 +69,137 @@ export class UnknownCollectionError extends Error { } } -export abstract class SchematicCommand extends Command { - readonly options: Option[] = []; +export abstract class SchematicCommand< + T extends BaseSchematicSchema & BaseCommandOptions +> extends Command { readonly allowPrivateSchematics: boolean = false; + readonly allowAdditionalArgs: boolean = false; private _host = new NodeJsSyncHost(); - private _workspace: experimental.workspace.Workspace; - private _deAliasedName: string; - private _originalOptions: Option[]; - private _engineHost: FileSystemEngineHostBase; - private _engine: Engine; - private _workflow: workflow.BaseWorkflow; - argStrategy = ArgumentStrategy.Nothing; - - constructor( - context: CommandContext, logger: logging.Logger, - engineHost: FileSystemEngineHostBase = new NodeModulesEngineHost()) { - super(context, logger); - this._engineHost = engineHost; - this._engine = new SchematicEngine(this._engineHost); - const registry = new schema.CoreSchemaRegistry(formats.standardFormats); - this._engineHost.registerOptionsTransform( - validateOptionsWithSchema(registry)); + private _workspace: workspaces.WorkspaceDefinition; + protected _workflow: NodeWorkflow; + + private readonly defaultCollectionName = '@schematics/angular'; + protected collectionName = this.defaultCollectionName; + protected schematicName?: string; + + constructor(context: CommandContext, description: CommandDescription, logger: logging.Logger) { + super(context, description, logger); } - protected readonly coreOptions: Option[] = [ - { - name: 'dryRun', - type: 'boolean', - default: false, - aliases: ['d'], - description: 'Run through without making any changes.', - }, - { - name: 'force', - type: 'boolean', - default: false, - aliases: ['f'], - description: 'Forces overwriting of files.', - }]; - - public async initialize(_options: any) { - this._loadWorkspace(); + public async initialize(options: T & Arguments) { + await this._loadWorkspace(); + this.createWorkflow(options); + + if (this.schematicName) { + // Set the options. + const collection = this.getCollection(this.collectionName); + const schematic = this.getSchematic(collection, this.schematicName, true); + const options = await parseJsonSchemaToOptions( + this._workflow.registry, + schematic.description.schemaJson || {}, + ); + + this.description.options.push(...options.filter(x => !x.hidden)); + + // Remove any user analytics from schematics that are NOT part of our safelist. + for (const o of this.description.options) { + if (o.userAnalytics) { + if (!isPackageNameSafeForAnalytics(this.collectionName)) { + o.userAnalytics = undefined; + } + } + } + } } - protected getEngineHost() { - return this._engineHost; + public async printHelp(options: T & Arguments) { + await super.printHelp(options); + this.logger.info(''); + + const subCommandOption = this.description.options.filter(x => x.subcommands)[0]; + + if (!subCommandOption || !subCommandOption.subcommands) { + return 0; + } + + const schematicNames = Object.keys(subCommandOption.subcommands); + + if (schematicNames.length > 1) { + this.logger.info('Available Schematics:'); + + const namesPerCollection: { [c: string]: string[] } = {}; + schematicNames.forEach(name => { + let [collectionName, schematicName] = name.split(/:/, 2); + if (!schematicName) { + schematicName = collectionName; + collectionName = this.collectionName; + } + + if (!namesPerCollection[collectionName]) { + namesPerCollection[collectionName] = []; + } + + namesPerCollection[collectionName].push(schematicName); + }); + + const defaultCollection = this.getDefaultSchematicCollection(); + Object.keys(namesPerCollection).forEach(collectionName => { + const isDefault = defaultCollection == collectionName; + this.logger.info(` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`); + + namesPerCollection[collectionName].forEach(schematicName => { + this.logger.info(` ${schematicName}`); + }); + }); + } else if (schematicNames.length == 1) { + this.logger.info('Help for schematic ' + schematicNames[0]); + await this.printHelpSubcommand(subCommandOption.subcommands[schematicNames[0]]); + } + + return 0; } - protected getEngine(): - Engine { - return this._engine; + + async printHelpUsage() { + const subCommandOption = this.description.options.filter(x => x.subcommands)[0]; + + if (!subCommandOption || !subCommandOption.subcommands) { + return; + } + + const schematicNames = Object.keys(subCommandOption.subcommands); + if (schematicNames.length == 1) { + this.logger.info(this.description.description); + + const opts = this.description.options.filter(x => x.positional === undefined); + const [collectionName, schematicName] = schematicNames[0].split(/:/)[0]; + + // Display if this is not the default collectionName, + // otherwise just show the schematicName. + const displayName = + collectionName == this.getDefaultSchematicCollection() ? schematicName : schematicNames[0]; + + const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options; + const schematicArgs = schematicOptions.filter(x => x.positional !== undefined); + const argDisplay = + schematicArgs.length > 0 + ? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ') + : ''; + + this.logger.info(tags.oneLine` + usage: ng ${this.description.name} ${displayName}${argDisplay} + ${opts.length > 0 ? `[options]` : ``} + `); + this.logger.info(''); + } else { + await super.printHelpUsage(); + } } - protected getCollection(collectionName: string): Collection { + protected getEngine(): FileSystemEngine { + return this._workflow.engine; + } + + protected getCollection(collectionName: string): FileSystemCollection { const engine = this.getEngine(); const collection = engine.createCollection(collectionName); @@ -140,113 +211,274 @@ export abstract class SchematicCommand extends Command { } protected getSchematic( - collection: Collection, schematicName: string, - allowPrivate?: boolean): Schematic { + collection: FileSystemCollection, + schematicName: string, + allowPrivate?: boolean, + ): FileSystemSchematic { return collection.createSchematic(schematicName, allowPrivate); } - protected setPathOptions(options: any, workingDir: string): any { + protected setPathOptions(options: Option[], workingDir: string) { if (workingDir === '') { return {}; } - return this.options + return options .filter(o => o.format === 'path') .map(o => o.name) - .filter(name => options[name] === undefined) - .reduce((acc: any, curr) => { - acc[curr] = workingDir; + .reduce( + (acc, curr) => { + acc[curr] = workingDir; - return acc; - }, {}); + return acc; + }, + {} as { [name: string]: string }, + ); } /* * Runtime hook to allow specifying customized workflow */ - protected getWorkflow(options: RunSchematicOptions): workflow.BaseWorkflow { - const {force, dryRun} = options; - const fsHost = new virtualFs.ScopedHost( - new NodeJsSyncHost(), normalize(this.project.root)); - - return new NodeWorkflow( - fsHost as any, - { - force, - dryRun, - packageManager: getPackageManager(), - root: this.project.root, - }, + protected createWorkflow(options: BaseSchematicSchema): workflow.BaseWorkflow { + if (this._workflow) { + return this._workflow; + } + + const { force, dryRun } = options; + const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.workspace.root)); + + const workflow = new NodeWorkflow(fsHost, { + force, + dryRun, + packageManager: getPackageManager(this.workspace.root), + root: normalize(this.workspace.root), + registry: new schema.CoreSchemaRegistry(formats.standardFormats), + }); + workflow.engineHost.registerContextTransform(context => { + // This is run by ALL schematics, so if someone uses `externalSchematics(...)` which + // is safelisted, it would move to the right analytics (even if their own isn't). + const collectionName: string = context.schematic.collection.description.name; + if (isPackageNameSafeForAnalytics(collectionName)) { + return { + ...context, + analytics: this.analytics, + }; + } else { + return context; + } + }); + + const getProjectName = () => { + if (this._workspace) { + const projectNames = getProjectsByPath(this._workspace, process.cwd(), this.workspace.root); + + 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.engineHost.registerOptionsTransform( + (schematic: FileSystemSchematicDescription, current: T) => ({ + ...getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName()), + ...current, + }), ); + + if (options.defaults) { + workflow.registry.addPreTransform(schema.transforms.addUndefinedDefaults); + } else { + workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); + } + + workflow.engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry)); + + workflow.registry.addSmartDefaultProvider('projectName', getProjectName); + + if (options.interactive !== false && isTTY()) { + workflow.registry.usePromptProvider((definitions: Array) => { + const questions: inquirer.Questions = definitions.map(definition => { + const question: inquirer.Question = { + name: definition.id, + message: definition.message, + default: definition.default, + }; + + const validator = definition.validator; + if (validator) { + question.validate = input => validator(input); + } + + switch (definition.type) { + case 'confirmation': + question.type = 'confirm'; + break; + case 'list': + question.type = !!definition.multiselect ? 'checkbox' : 'list'; + question.choices = + definition.items && + definition.items.map(item => { + if (typeof item == 'string') { + return item; + } else { + return { + name: item.label, + value: item.value, + }; + } + }); + break; + default: + question.type = definition.type; + break; + } + + return question; + }); + + return inquirer.prompt(questions); + }); + } + + return (this._workflow = workflow); } - private _getWorkflow(options: RunSchematicOptions): workflow.BaseWorkflow { - if (!this._workflow) { - this._workflow = this.getWorkflow(options); + protected getDefaultSchematicCollection(): string { + let workspace = getWorkspace('local'); + + if (workspace) { + const project = getProjectByCwd(workspace); + if (project && workspace.getProjectCli(project)) { + const value = workspace.getProjectCli(project)['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + if (workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + } + + workspace = getWorkspace('global'); + if (workspace && workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } } - return this._workflow; + return this.defaultCollectionName; } - protected runSchematic(options: RunSchematicOptions) { - const {collectionName, schematicName, debug, dryRun} = options; - let schematicOptions = this.removeCoreOptions(options.schematicOptions); + 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._getWorkflow(options); - const workingDir = process.cwd().replace(this.project.root, '').replace(/\\/g, '/'); - const pathOptions = this.setPathOptions(schematicOptions, workingDir); - schematicOptions = { ...schematicOptions, ...pathOptions }; - const defaultOptions = this.readDefaults(collectionName, schematicName, schematicOptions); - schematicOptions = { ...schematicOptions, ...defaultOptions }; + const workflow = this._workflow; - // Remove all of the original arguments which have already been parsed + const workingDir = normalize(systemPath.relative(this.workspace.root, process.cwd())); - const argumentCount = this._originalOptions - .filter(opt => { - let isArgument = false; - if (opt.$default !== undefined && opt.$default.$source === 'argv') { - isArgument = true; + // Get the option object from the schematic schema. + const schematic = this.getSchematic( + this.getCollection(collectionName), + schematicName, + this.allowPrivateSchematics, + ); + // Update the schematic and collection name in case they're not the same as the ones we + // received in our options, e.g. after alias resolution or extension. + collectionName = schematic.collection.description.name; + schematicName = schematic.description.name; + + // TODO: Remove warning check when 'targets' is default + if (collectionName !== this.defaultCollectionName) { + const [ast, configPath] = getWorkspaceRaw('local'); + if (ast) { + const projectsKeyValue = ast.properties.find(p => p.key.value === 'projects'); + if (!projectsKeyValue || projectsKeyValue.value.kind !== 'object') { + return; } - return isArgument; - }) - .length; + const positions: json.Position[] = []; + for (const projectKeyValue of projectsKeyValue.value.properties) { + const projectNode = projectKeyValue.value; + if (projectNode.kind !== 'object') { + continue; + } + const targetsKeyValue = projectNode.properties.find(p => p.key.value === 'targets'); + if (targetsKeyValue) { + positions.push(targetsKeyValue.start); + } + } - // Pass the rest of the arguments as the smart default "argv". Then delete it. - const rawArgs = schematicOptions._.slice(argumentCount); - workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { - if ('index' in schema) { - return rawArgs[Number(schema['index'])]; - } else { - return rawArgs; - } - }); - delete schematicOptions._; + if (positions.length > 0) { + const warning = tags.oneLine` + WARNING: This command may not execute successfully. + The package/collection may not support the 'targets' field within '${configPath}'. + This can be corrected by renaming the following 'targets' fields to 'architect': + `; - workflow.registry.addSmartDefaultProvider('projectName', (_schema: JsonObject) => { - if (this._workspace) { - try { - return this._workspace.getProjectByPath(normalize(process.cwd())) - || this._workspace.getDefaultProjectName(); - } catch (e) { - if (e instanceof experimental.workspace.AmbiguousProjectPathException) { - this.logger.warn(tags.oneLine` - Two or more projects are using identical roots. - Unable to determine project using current working directory. - Using default workspace project instead. - `); + const locations = positions + .map((p, i) => `${i + 1}) Line: ${p.line + 1}; Column: ${p.character + 1}`) + .join('\n'); - return this._workspace.getDefaultProjectName(); - } - throw e; + this.logger.warn(warning + '\n' + locations + '\n'); } } + } - return undefined; - }); + // Set the options of format "path". + let o: Option[] | null = null; + let args: Arguments; + + if (!schematic.description.schemaJson) { + args = await this.parseFreeFormArguments(schematicOptions || []); + } else { + o = await parseJsonSchemaToOptions(workflow.registry, schematic.description.schemaJson); + args = await this.parseArguments(schematicOptions || [], o); + } + + // ng-add is special because we don't know all possible options at this point + if (args['--'] && !this.allowAdditionalArgs) { + args['--'].forEach(additional => { + this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); + }); + + return 1; + } + + const pathOptions = o ? this.setPathOptions(o, workingDir) : {}; + let input = { ...pathOptions, ...args }; + + // Read the default values from the workspace. + const projectName = input.project !== undefined ? '' + input.project : null; + const defaults = getSchematicDefaults(collectionName, schematicName, projectName); + input = { + ...defaults, + ...input, + ...options.additionalOptions, + }; workflow.reporter.subscribe((event: DryRunEvent) => { nothingDone = false; @@ -262,19 +494,19 @@ export abstract class SchematicCommand extends Command { break; case 'update': loggingQueue.push(tags.oneLine` - ${terminal.white('UPDATE')} ${eventPath} (${event.content.length} bytes) + ${colors.white('UPDATE')} ${eventPath} (${event.content.length} bytes) `); break; case 'create': loggingQueue.push(tags.oneLine` - ${terminal.green('CREATE')} ${eventPath} (${event.content.length} bytes) + ${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes) `); break; case 'delete': - loggingQueue.push(`${terminal.yellow('DELETE')} ${eventPath}`); + loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`); break; case 'rename': - loggingQueue.push(`${terminal.blue('RENAME')} ${eventPath} => ${event.to}`); + loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${event.to}`); break; } }); @@ -291,124 +523,66 @@ export abstract class SchematicCommand extends Command { } }); - return new Promise((resolve) => { - workflow.execute({ - collection: collectionName, - schematic: schematicName, - options: schematicOptions, - debug: debug, - logger: this.logger as any, - allowPrivate: this.allowPrivateSchematics, - }) - .subscribe({ - error: (err: Error) => { - // In case the workflow was not successful, show an appropriate error message. - if (err instanceof UnsuccessfulWorkflowExecution) { - // "See above" because we already printed the error. - this.logger.fatal('The Schematic workflow failed. See above.'); - } else if (debug) { - this.logger.fatal(`An error occured:\n${err.message}\n${err.stack}`); - } else { - this.logger.fatal(err.message); - } + return new Promise(resolve => { + workflow + .execute({ + collection: collectionName, + schematic: schematicName, + options: input, + debug: debug, + logger: this.logger, + allowPrivate: this.allowPrivateSchematics, + }) + .subscribe({ + error: (err: Error) => { + // In case the workflow was not successful, show an appropriate error message. + if (err instanceof UnsuccessfulWorkflowExecution) { + // "See above" because we already printed the error. + this.logger.fatal('The Schematic workflow failed. See above.'); + } else if (debug) { + this.logger.fatal(`An error occured:\n${err.message}\n${err.stack}`); + } else { + this.logger.fatal(err.message); + } - resolve(1); - }, - complete: () => { - const showNothingDone = !(options.showNothingDone === false); - if (nothingDone && showNothingDone) { - this.logger.info('Nothing to be done.'); - } - if (dryRun) { - this.logger.warn(`\nNOTE: Run with "dry run" no changes were made.`); - } - resolve(); - }, - }); + 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 removeCoreOptions(options: any): any { - const opts = Object.assign({}, options); - if (this._originalOptions.find(option => option.name == 'dryRun')) { - delete opts.dryRun; - } - if (this._originalOptions.find(option => option.name == 'force')) { - delete opts.force; - } - if (this._originalOptions.find(option => option.name == 'debug')) { - delete opts.debug; - } - - return opts; + protected async parseFreeFormArguments(schematicOptions: string[]) { + return parseFreeFormArguments(schematicOptions); } - protected getOptions(options: GetOptionsOptions): Promise { - // Make a copy. - this._originalOptions = [...this.options]; - - const collectionName = options.collectionName || getDefaultSchematicCollection(); - - const collection = this.getCollection(collectionName); - - const schematic = this.getSchematic(collection, options.schematicName, - this.allowPrivateSchematics); - this._deAliasedName = schematic.description.name; - - if (!schematic.description.schemaJson) { - return Promise.resolve([]); - } - - const properties = schematic.description.schemaJson.properties; - const keys = Object.keys(properties); - const availableOptions = keys - .map(key => ({ ...properties[key], ...{ name: strings.dasherize(key) } })) - .map(opt => { - const types = ['string', 'boolean', 'integer', 'number']; - // Ignore arrays / objects. - if (types.indexOf(opt.type) === -1) { - return null; - } - - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x); - - return Promise.resolve(availableOptions); + protected async parseArguments( + schematicOptions: string[], + options: Option[] | null, + ): Promise { + return parseArguments(schematicOptions, options, this.logger); } - private _loadWorkspace() { + private async _loadWorkspace() { if (this._workspace) { return; } - const workspaceLoader = new WorkspaceLoader(this._host); try { - workspaceLoader.loadWorkspace(this.project.root).pipe(take(1)) - .subscribe( - (workspace: experimental.workspace.Workspace) => this._workspace = workspace, - (err: Error) => { - if (!this.allowMissingWorkspace) { - // Ignore missing workspace - throw err; - } - }, - ); + const { workspace } = await workspaces.readWorkspace( + this.workspace.root, + workspaces.createWorkspaceHost(this._host), + ); + this._workspace = workspace; } catch (err) { if (!this.allowMissingWorkspace) { // Ignore missing workspace @@ -416,33 +590,43 @@ export abstract class SchematicCommand extends Command { } } } +} - private _cleanDefaults(defaults: T, undefinedOptions: string[]): T { - (Object.keys(defaults) as K[]) - .filter(key => !undefinedOptions.map(strings.camelize).includes(key as string)) - .forEach(key => { - delete defaults[key]; - }); - - return defaults; +function getProjectsByPath( + workspace: workspaces.WorkspaceDefinition, + path: string, + root: string, +): string[] { + if (workspace.projects.size === 1) { + return Array.from(workspace.projects.keys()); } - private readDefaults(collectionName: string, schematicName: string, options: any): {} { - if (this._deAliasedName) { - schematicName = this._deAliasedName; + 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; } - const projectName = options.project; - const defaults = getSchematicDefaults(collectionName, schematicName, projectName); + return false; + }; - // Get list of all undefined options. - const undefinedOptions = this.options - .filter(o => options[o.name] === undefined) - .map(o => o.name); + 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); - // Delete any default that is not undefined. - this._cleanDefaults(defaults, undefinedOptions); + if (projects.length === 1) { + return [projects[0][1]]; + } else if (projects.length > 1) { + const firstPath = projects[0][0]; - return defaults; + return projects.filter(v => v[0] === firstPath).map(v => v[1]); } + + return []; } diff --git a/packages/angular/cli/models/workspace-loader.ts b/packages/angular/cli/models/workspace-loader.ts deleted file mode 100644 index e8785f7dc209..000000000000 --- a/packages/angular/cli/models/workspace-loader.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { - Path, - basename, - dirname, - experimental, - join, - normalize, - virtualFs, -} from '@angular-devkit/core'; -import * as fs from 'fs'; -import { homedir } from 'os'; -import { Observable, of } from 'rxjs'; -import { concatMap, tap } from 'rxjs/operators'; -import { findUp } from '../utilities/find-up'; - - -// TODO: error out instead of returning null when workspace cannot be found. -export class WorkspaceLoader { - private _workspaceCacheMap = new Map(); - // TODO: add remaining fallbacks. - private _configFileNames = [ - normalize('.angular.json'), - normalize('angular.json'), - ]; - constructor(private _host: virtualFs.Host) { } - - loadGlobalWorkspace(): Observable { - return this._getGlobalWorkspaceFilePath().pipe( - concatMap(globalWorkspacePath => this._loadWorkspaceFromPath(globalWorkspacePath)), - ); - } - - loadWorkspace(projectPath?: string): Observable { - return this._getProjectWorkspaceFilePath(projectPath).pipe( - concatMap(globalWorkspacePath => this._loadWorkspaceFromPath(globalWorkspacePath)), - ); - } - - // TODO: do this with the host instead of fs. - private _getProjectWorkspaceFilePath(projectPath?: string): Observable { - // Find the workspace file, either where specified, in the Angular CLI project - // (if it's in node_modules) or from the current process. - const workspaceFilePath = (projectPath && findUp(this._configFileNames, projectPath)) - || findUp(this._configFileNames, process.cwd()) - || findUp(this._configFileNames, __dirname); - - if (workspaceFilePath) { - return of(normalize(workspaceFilePath)); - } else { - throw new Error(`Local workspace file ('angular.json') could not be found.`); - } - } - - // TODO: do this with the host instead of fs. - private _getGlobalWorkspaceFilePath(): Observable { - for (const fileName of this._configFileNames) { - const workspaceFilePath = join(normalize(homedir()), fileName); - - if (fs.existsSync(workspaceFilePath)) { - return of(normalize(workspaceFilePath)); - } - } - - return of(null); - } - - private _loadWorkspaceFromPath(workspacePath: Path | null) { - if (!workspacePath) { - return of(null); - } - - if (this._workspaceCacheMap.has(workspacePath)) { - return of(this._workspaceCacheMap.get(workspacePath) || null); - } - - const workspaceRoot = dirname(workspacePath); - const workspaceFileName = basename(workspacePath); - const workspace = new experimental.workspace.Workspace(workspaceRoot, this._host); - - return workspace.loadWorkspaceFromHost(workspaceFileName).pipe( - tap(workspace => this._workspaceCacheMap.set(workspacePath, workspace)), - ); - } -} diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 38b778a03d0d..90dadff8f2ab 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -3,7 +3,6 @@ "version": "0.0.0", "description": "CLI tool for Angular", "main": "lib/cli/index.js", - "trackingCode": "UA-8594346-19", "bin": { "ng": "./bin/ng" }, @@ -13,16 +12,12 @@ "Angular CLI" ], "scripts": { - "postinstall": "node ./bin/ng-update-message.js" + "postinstall": "node ./bin/postinstall/script.js" }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, - "engines": { - "node": ">= 8.9.0", - "npm": ">= 5.5.1" - }, "author": "Angular Authors", "license": "MIT", "bugs": { @@ -35,15 +30,27 @@ "@angular-devkit/schematics": "0.0.0", "@schematics/angular": "0.0.0", "@schematics/update": "0.0.0", - "json-schema-traverse": "^0.4.1", - "opn": "^5.3.0", - "json-schema-traverse": "^0.4.1", - "rxjs": "^6.0.0", - "semver": "^5.1.0", - "symbol-observable": "^1.2.0", - "yargs-parser": "^10.0.0" + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "^4.1.1", + "ini": "1.3.5", + "inquirer": "6.5.0", + "npm-package-arg": "6.1.0", + "open": "6.4.0", + "pacote": "9.5.4", + "read-package-tree": "5.3.1", + "semver": "6.3.0", + "symbol-observable": "1.2.0", + "universal-analytics": "^0.4.20", + "uuid": "^3.3.2" }, "ng-update": { - "migrations": "@schematics/angular/migrations/migration-collection.json" + "migrations": "@schematics/angular/migrations/migration-collection.json", + "packageGroup": { + "@angular/cli": "0.0.0", + "@angular-devkit/build-angular": "0.0.0", + "@angular-devkit/build-ng-packagr": "0.0.0", + "@angular-devkit/build-webpack": "0.0.0" + } } } diff --git a/packages/angular/cli/tasks/npm-install.ts b/packages/angular/cli/tasks/npm-install.ts index 243587054b88..f60fce0f8991 100644 --- a/packages/angular/cli/tasks/npm-install.ts +++ b/packages/angular/cli/tasks/npm-install.ts @@ -6,27 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import { logging, terminal } from '@angular-devkit/core'; -import { ModuleNotFoundException, resolve } from '@angular-devkit/core/node'; +import { logging } from '@angular-devkit/core'; import { spawn } from 'child_process'; - - -export type NpmInstall = (packageName: string, - logger: logging.Logger, - packageManager: string, - projectRoot: string, - save?: boolean) => Promise; - -export default async function (packageName: string, - logger: logging.Logger, - packageManager: string, - projectRoot: string, - save = true) { +import { colors } from '../utilities/color'; + +export default async function( + packageName: string, + logger: logging.Logger, + packageManager: string, + projectRoot: string, + save = true, +) { const installArgs: string[] = []; switch (packageManager) { case 'cnpm': + case 'pnpm': case 'npm': - installArgs.push('install', '--quiet'); + installArgs.push('install'); break; case 'yarn': @@ -35,46 +31,33 @@ export default async function (packageName: string, default: packageManager = 'npm'; - installArgs.push('install', '--quiet'); + installArgs.push('install'); break; } - logger.info(terminal.green(`Installing packages for tooling via ${packageManager}.`)); + logger.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); if (packageName) { - try { - // Verify if we need to install the package (it might already be there). - // If it's available and we shouldn't save, simply return. Nothing to be done. - resolve(packageName, { checkLocal: true, basedir: projectRoot }); - - return; - } catch (e) { - if (!(e instanceof ModuleNotFoundException)) { - throw e; - } - } installArgs.push(packageName); } if (!save) { installArgs.push('--no-save'); } - const installOptions = { - stdio: 'inherit', - shell: true, - }; + + installArgs.push('--quiet'); await new Promise((resolve, reject) => { - spawn(packageManager, installArgs, installOptions) - .on('close', (code: number) => { + spawn(packageManager, installArgs, { stdio: 'inherit', shell: true }).on( + 'close', + (code: number) => { if (code === 0) { - logger.info(terminal.green(`Installed packages for tooling via ${packageManager}.`)); + logger.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); resolve(); } else { - const message = 'Package install failed, see above.'; - logger.info(terminal.red(message)); - reject(message); + reject('Package install failed, see above.'); } - }); + }, + ); }); } diff --git a/packages/angular/cli/upgrade/version.ts b/packages/angular/cli/upgrade/version.ts deleted file mode 100644 index a24b87231fd1..000000000000 --- a/packages/angular/cli/upgrade/version.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { tags, terminal } from '@angular-devkit/core'; -import { resolve } from '@angular-devkit/core/node'; -import * as path from 'path'; -import { SemVer, satisfies } from 'semver'; -import { isWarningEnabled } from '../utilities/config'; - - -export class Version { - private _semver: SemVer | null = null; - constructor(private _version: string | null = null) { - this._semver = _version ? new SemVer(_version) : null; - } - - isAlpha() { return this.qualifier == 'alpha'; } - isBeta() { return this.qualifier == 'beta'; } - isReleaseCandidate() { return this.qualifier == 'rc'; } - isKnown() { return this._version !== null; } - - isLocal() { return this.isKnown() && this._version && path.isAbsolute(this._version); } - isGreaterThanOrEqualTo(other: SemVer) { - return this._semver !== null && this._semver.compare(other) >= 0; - } - - get major() { return this._semver ? this._semver.major : 0; } - get minor() { return this._semver ? this._semver.minor : 0; } - get patch() { return this._semver ? this._semver.patch : 0; } - get qualifier() { return this._semver ? this._semver.prerelease[0] : ''; } - get extra() { return this._semver ? this._semver.prerelease[1] : ''; } - - toString() { return this._version; } - - static assertCompatibleAngularVersion(projectRoot: string) { - let angularPkgJson; - let rxjsPkgJson; - - const isInside = (base: string, potential: string): boolean => { - const absoluteBase = path.resolve(base); - const absolutePotential = path.resolve(potential); - const relativePotential = path.relative(absoluteBase, absolutePotential); - if (!relativePotential.startsWith('..') && !path.isAbsolute(relativePotential)) { - return true; - } - - return false; - }; - - try { - const resolveOptions = { - basedir: projectRoot, - checkGlobal: false, - checkLocal: true, - }; - const angularPackagePath = resolve('@angular/core/package.json', resolveOptions); - const rxjsPackagePath = resolve('rxjs/package.json', resolveOptions); - - if (!isInside(projectRoot, angularPackagePath) - || !isInside(projectRoot, rxjsPackagePath)) { - throw new Error(); - } - - angularPkgJson = require(angularPackagePath); - rxjsPkgJson = require(rxjsPackagePath); - } catch { - console.error(terminal.bold(terminal.red(tags.stripIndents` - You seem to not be depending on "@angular/core" and/or "rxjs". This is an error. - `))); - process.exit(2); - } - - if (!(angularPkgJson && angularPkgJson['version'] && rxjsPkgJson && rxjsPkgJson['version'])) { - console.error(terminal.bold(terminal.red(tags.stripIndents` - Cannot determine versions of "@angular/core" and/or "rxjs". - This likely means your local installation is broken. Please reinstall your packages. - `))); - process.exit(2); - } - - const angularVersion = new Version(angularPkgJson['version']); - const rxjsVersion = new Version(rxjsPkgJson['version']); - - if (angularVersion.isLocal()) { - console.error(terminal.yellow('Using a local version of angular. Proceeding with care...')); - - return; - } - - if (!angularVersion.isGreaterThanOrEqualTo(new SemVer('5.0.0'))) { - console.error(terminal.bold(terminal.red(tags.stripIndents` - This version of CLI is only compatible with Angular version 5.0.0 or higher. - - Please visit the link below to find instructions on how to update Angular. - https://angular-update-guide.firebaseapp.com/ - ` + '\n'))); - process.exit(3); - } else if ( - angularVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-rc.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('5.6.0-forward-compat.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-beta.0')) - ) { - console.error(terminal.bold(terminal.red(tags.stripIndents` - This project uses version ${rxjsVersion} of RxJs, which is not supported by Angular v6. - The official RxJs version that is supported is 5.6.0-forward-compat.0 and greater. - - Please visit the link below to find instructions on how to update RxJs. - https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# - ` + '\n'))); - process.exit(3); - } else if ( - angularVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-rc.0')) - && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-beta.0')) - ) { - console.warn(terminal.bold(terminal.red(tags.stripIndents` - This project uses a temporary compatibility version of RxJs (${rxjsVersion}). - - Please visit the link below to find instructions on how to update RxJs. - https://docs.google.com/document/d/12nlLt71VLKb-z3YaSGzUfx6mJbc34nsMXtByPUN35cg/edit# - ` + '\n'))); - } - } - - static assertTypescriptVersion(projectRoot: string) { - if (!isWarningEnabled('typescriptMismatch')) { - return; - } - - let compilerVersion: string; - let tsVersion: string; - let compilerTypeScriptPeerVersion: string; - try { - const resolveOptions = { - basedir: projectRoot, - checkGlobal: false, - checkLocal: true, - }; - const compilerPackagePath = resolve('@angular/compiler-cli/package.json', resolveOptions); - const typescriptProjectPath = resolve('typescript', resolveOptions); - const compilerPackageInfo = require(compilerPackagePath); - - compilerVersion = compilerPackageInfo['version']; - compilerTypeScriptPeerVersion = compilerPackageInfo['peerDependencies']['typescript']; - tsVersion = require(typescriptProjectPath).version; - } catch { - console.error(terminal.bold(terminal.red(tags.stripIndents` - Versions of @angular/compiler-cli and typescript could not be determined. - The most common reason for this is a broken npm install. - - Please make sure your package.json contains both @angular/compiler-cli and typescript in - devDependencies, then delete node_modules and package-lock.json (if you have one) and - run npm install again. - `))); - process.exit(2); - - return; - } - - // These versions do not have accurate typescript peer dependencies - const versionCombos = [ - { compiler: '>=2.3.1 <3.0.0', typescript: '>=2.0.2 <2.3.0' }, - { compiler: '>=4.0.0-beta.0 <5.0.0', typescript: '>=2.1.0 <2.4.0' }, - { compiler: '5.0.0-beta.0 - 5.0.0-rc.2', typescript: '>=2.4.2 <2.5.0' }, - ]; - - let currentCombo = versionCombos.find((combo) => satisfies(compilerVersion, combo.compiler)); - if (!currentCombo && compilerTypeScriptPeerVersion) { - currentCombo = { compiler: compilerVersion, typescript: compilerTypeScriptPeerVersion }; - } - - if (currentCombo && !satisfies(tsVersion, currentCombo.typescript)) { - // First line of warning looks weird being split in two, disable tslint for it. - console.error((terminal.yellow('\n' + tags.stripIndent` - @angular/compiler-cli@${compilerVersion} requires typescript@'${ - currentCombo.typescript}' but ${tsVersion} was found instead. - Using this version can result in undefined behaviour and difficult to debug problems. - - Please run the following command to install a compatible version of TypeScript. - - npm install typescript@'${currentCombo.typescript}' - - To disable this warning run "ng config cli.warnings.typescriptMismatch false". - ` + '\n'))); - } - } - -} diff --git a/packages/angular/cli/utilities/bep.ts b/packages/angular/cli/utilities/bep.ts new file mode 100644 index 000000000000..8acb5ec7cd3b --- /dev/null +++ b/packages/angular/cli/utilities/bep.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as fs from 'fs'; + + +export interface BuildEventMessage { + id: {}; + [key: string]: {}; +} + +export class BepGenerator { + private constructor() {} + + static createBuildStarted(command: string, time?: number): BuildEventMessage { + return { + id: { started: {} }, + started: { + command, + start_time_millis: time == undefined ? Date.now() : time, + }, + }; + } + + static createBuildFinished(code: number, time?: number): BuildEventMessage { + return { + id: { finished: {} }, + finished: { + finish_time_millis: time == undefined ? Date.now() : time, + exit_code: { code }, + }, + }; + } +} + +export class BepJsonWriter { + private stream = fs.createWriteStream(this.filename); + + constructor(public readonly filename: string) { + + } + + close(): void { + this.stream.close(); + } + + writeEvent(event: BuildEventMessage): void { + const raw = JSON.stringify(event); + + this.stream.write(raw + '\n'); + } + + writeBuildStarted(command: string, time?: number): void { + const event = BepGenerator.createBuildStarted(command, time); + + this.writeEvent(event); + } + + writeBuildFinished(code: number, time?: number): void { + const event = BepGenerator.createBuildFinished(code, time); + + this.writeEvent(event); + } +} diff --git a/packages/angular/cli/utilities/check-package-manager.ts b/packages/angular/cli/utilities/check-package-manager.ts deleted file mode 100644 index b62c89a1fbe2..000000000000 --- a/packages/angular/cli/utilities/check-package-manager.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { terminal } from '@angular-devkit/core'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { getPackageManager } from './config'; - -const execPromise = promisify(exec); -const packageManager = getPackageManager(); - - -export function checkYarnOrCNPM() { - - // Don't show messages if user has already changed the default. - if (packageManager !== 'default') { - return Promise.resolve(); - } - - return Promise - .all([checkYarn(), checkCNPM()]) - .then((data: Array) => { - const [isYarnInstalled, isCNPMInstalled] = data; - if (isYarnInstalled && isCNPMInstalled) { - console.error(terminal.yellow('You can `ng config -g cli.packageManager yarn` ' - + 'or `ng config -g cli.packageManager cnpm`.')); - } else if (isYarnInstalled) { - console.error(terminal.yellow('You can `ng config -g cli.packageManager yarn`.')); - } else if (isCNPMInstalled) { - console.error(terminal.yellow('You can `ng config -g cli.packageManager cnpm`.')); - } else { - if (packageManager !== 'default' && packageManager !== 'npm') { - console.error(terminal.yellow(`Seems that ${packageManager} is not installed.`)); - console.error(terminal.yellow('You can `ng config -g cli.packageManager npm`.')); - } - } - }); -} - -function checkYarn() { - return execPromise('yarn --version') - .then(() => true, () => false); -} - -function checkCNPM() { - return execPromise('cnpm --version') - .then(() => true, () => false); -} diff --git a/packages/angular/cli/utilities/color.ts b/packages/angular/cli/utilities/color.ts new file mode 100644 index 000000000000..59344dfa95f3 --- /dev/null +++ b/packages/angular/cli/utilities/color.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as colors from 'ansi-colors'; +import { WriteStream } from 'tty'; + +// Typings do not contain the function call (added in Node.js v9.9.0) +export const supportsColor = + process.stdout instanceof WriteStream && + ((process.stdout as unknown) as { getColorDepth(): number }).getColorDepth() > 1; + +(colors as { enabled: boolean }).enabled = supportsColor; + +export { colors }; diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts index d9fdb26e9aaf..6f653a05ad5b 100644 --- a/packages/angular/cli/utilities/config.ts +++ b/packages/angular/cli/utilities/config.ts @@ -78,7 +78,18 @@ export function getWorkspace( new NodeJsSyncHost(), ); - workspace.loadWorkspaceFromHost(file).subscribe(); + let error: unknown; + workspace.loadWorkspaceFromHost(file).subscribe({ + error: e => error = e, + }); + + if (error) { + throw new Error( + `Workspace config file cannot le loaded: ${configPath}` + + `\n${error instanceof Error ? error.message : error}`, + ); + } + cachedWorkspaces.set(level, workspace); return workspace; @@ -116,7 +127,7 @@ export function getWorkspaceRaw( const ast = parseJsonAst(content, JsonParseMode.Loose); if (ast.kind != 'object') { - throw new Error('Invalid JSON'); + throw new Error(`Invalid JSON file: ${configPath}`); } return [ast, configPath]; @@ -140,7 +151,7 @@ export function validateWorkspace(json: JsonObject) { return true; } -function getProjectByCwd(workspace: experimental.workspace.Workspace): string | null { +export function getProjectByCwd(workspace: experimental.workspace.Workspace): string | null { try { return workspace.getProjectByPath(normalize(process.cwd())); } catch (e) { @@ -151,7 +162,7 @@ function getProjectByCwd(workspace: experimental.workspace.Workspace): string | } } -export function getPackageManager(): string { +export function getConfiguredPackageManager(): string | null { let workspace = getWorkspace('local'); if (workspace) { @@ -186,7 +197,7 @@ export function getPackageManager(): string { } } - return 'npm'; + return null; } export function migrateLegacyGlobalConfig(): boolean { @@ -221,9 +232,6 @@ export function migrateLegacyGlobalConfig(): boolean { if (typeof legacy.warnings.versionMismatch == 'boolean') { warnings['versionMismatch'] = legacy.warnings.versionMismatch; } - if (typeof legacy.warnings.typescriptMismatch == 'boolean') { - warnings['typescriptMismatch'] = legacy.warnings.typescriptMismatch; - } if (Object.getOwnPropertyNames(warnings).length > 0) { cli['warnings'] = warnings; @@ -265,36 +273,6 @@ function getLegacyPackageManager(): string | null { return null; } -export function getDefaultSchematicCollection(): string { - let workspace = getWorkspace('local'); - - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const value = workspace.getProjectCli(project)['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - if (workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - - return '@schematics/angular'; -} - export function getSchematicDefaults( collection: string, schematic: string, diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts new file mode 100644 index 000000000000..c91a1e213197 --- /dev/null +++ b/packages/angular/cli/utilities/json-schema.ts @@ -0,0 +1,296 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { BaseException, json } from '@angular-devkit/core'; +import { ExportStringRef } from '@angular-devkit/schematics/tools'; +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { + CommandConstructor, + CommandDescription, + CommandScope, + Option, + OptionType, + SubCommandDescription, + Value, +} from '../models/interface'; + + +export class CommandJsonPathException extends BaseException { + constructor(public readonly path: string, public readonly name: string) { + super(`File ${path} was not found while constructing the subcommand ${name}.`); + } +} + +function _getEnumFromValue( + value: json.JsonValue, + enumeration: E, + defaultValue: T, +): T { + if (typeof value !== 'string') { + return defaultValue; + } + + if (Object.values(enumeration).indexOf(value) !== -1) { + // TODO: this should be unknown + // tslint:disable-next-line:no-any + return value as any as T; + } + + return defaultValue; +} + +export async function parseJsonSchemaToSubCommandDescription( + name: string, + jsonPath: string, + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + const options = await parseJsonSchemaToOptions(registry, schema); + + const aliases: string[] = []; + if (json.isJsonArray(schema.$aliases)) { + schema.$aliases.forEach(value => { + if (typeof value == 'string') { + aliases.push(value); + } + }); + } + if (json.isJsonArray(schema.aliases)) { + schema.aliases.forEach(value => { + if (typeof value == 'string') { + aliases.push(value); + } + }); + } + if (typeof schema.alias == 'string') { + aliases.push(schema.alias); + } + + let longDescription = ''; + if (typeof schema.$longDescription == 'string' && schema.$longDescription) { + const ldPath = resolve(dirname(jsonPath), schema.$longDescription); + try { + longDescription = readFileSync(ldPath, 'utf-8'); + } catch (e) { + throw new CommandJsonPathException(ldPath, name); + } + } + let usageNotes = ''; + if (typeof schema.$usageNotes == 'string' && schema.$usageNotes) { + const unPath = resolve(dirname(jsonPath), schema.$usageNotes); + try { + usageNotes = readFileSync(unPath, 'utf-8'); + } catch (e) { + throw new CommandJsonPathException(unPath, name); + } + } + + const description = '' + (schema.description === undefined ? '' : schema.description); + + return { + name, + description, + ...(longDescription ? { longDescription } : {}), + ...(usageNotes ? { usageNotes } : {}), + options, + aliases, + }; +} + +export async function parseJsonSchemaToCommandDescription( + name: string, + jsonPath: string, + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + const subcommand = + await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema); + + // Before doing any work, let's validate the implementation. + if (typeof schema.$impl != 'string') { + throw new Error(`Command ${name} has an invalid implementation.`); + } + const ref = new ExportStringRef(schema.$impl, dirname(jsonPath)); + const impl = ref.ref; + + if (impl === undefined || typeof impl !== 'function') { + throw new Error(`Command ${name} has an invalid implementation.`); + } + + const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default); + const hidden = !!schema.$hidden; + + return { + ...subcommand, + scope, + hidden, + impl, + }; +} + +export async function parseJsonSchemaToOptions( + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + const options: Option[] = []; + + function visitor( + current: json.JsonObject | json.JsonArray, + pointer: json.schema.JsonPointer, + parentSchema?: json.JsonObject | json.JsonArray, + ) { + if (!parentSchema) { + // Ignore root. + return; + } else if (pointer.split(/\/(?:properties|items|definitions)\//g).length > 2) { + // Ignore subitems (objects or arrays). + return; + } else if (json.isJsonArray(current)) { + return; + } + + if (pointer.indexOf('/not/') != -1) { + // We don't support anyOf/not. + throw new Error('The "not" keyword is not supported in JSON Schema.'); + } + + const ptr = json.schema.parseJsonPointer(pointer); + const name = ptr[ptr.length - 1]; + + if (ptr[ptr.length - 2] != 'properties') { + // Skip any non-property items. + return; + } + + const typeSet = json.schema.getTypesOfSchema(current); + + if (typeSet.size == 0) { + throw new Error('Cannot find type of schema.'); + } + + // We only support number, string or boolean (or array of those), so remove everything else. + const types = [...typeSet].filter(x => { + switch (x) { + case 'boolean': + case 'number': + case 'string': + return true; + + case 'array': + // Only include arrays if they're boolean, string or number. + if (json.isJsonObject(current.items) + && typeof current.items.type == 'string' + && ['boolean', 'number', 'string'].includes(current.items.type)) { + return true; + } + + return false; + + default: + return false; + } + }).map(x => _getEnumFromValue(x, OptionType, OptionType.String)); + + if (types.length == 0) { + // This means it's not usable on the command line. e.g. an Object. + return; + } + + // Only keep enum values we support (booleans, numbers and strings). + const enumValues = (json.isJsonArray(current.enum) && current.enum || []).filter(x => { + switch (typeof x) { + case 'boolean': + case 'number': + case 'string': + return true; + + default: + return false; + } + }) as Value[]; + + let defaultValue: string | number | boolean | undefined = undefined; + if (current.default !== undefined) { + switch (types[0]) { + case 'string': + if (typeof current.default == 'string') { + defaultValue = current.default; + } + break; + case 'number': + if (typeof current.default == 'number') { + defaultValue = current.default; + } + break; + case 'boolean': + if (typeof current.default == 'boolean') { + defaultValue = current.default; + } + break; + } + } + + const type = types[0]; + const $default = current.$default; + const $defaultIndex = (json.isJsonObject($default) && $default['$source'] == 'argv') + ? $default['index'] : undefined; + const positional: number | undefined = typeof $defaultIndex == 'number' + ? $defaultIndex : undefined; + + const required = json.isJsonArray(current.required) + ? current.required.indexOf(name) != -1 : false; + const aliases = json.isJsonArray(current.aliases) ? [...current.aliases].map(x => '' + x) + : current.alias ? ['' + current.alias] : []; + const format = typeof current.format == 'string' ? current.format : undefined; + const visible = current.visible === undefined || current.visible === true; + const hidden = !!current.hidden || !visible; + + // Deprecated is set only if it's true or a string. + const xDeprecated = current['x-deprecated']; + const deprecated = (xDeprecated === true || typeof xDeprecated == 'string') + ? xDeprecated : undefined; + + const xUserAnalytics = current['x-user-analytics']; + const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined; + + const option: Option = { + name, + description: '' + (current.description === undefined ? '' : current.description), + ...types.length == 1 ? { type } : { type, types }, + ...defaultValue !== undefined ? { default: defaultValue } : {}, + ...enumValues && enumValues.length > 0 ? { enum: enumValues } : {}, + required, + aliases, + ...format !== undefined ? { format } : {}, + hidden, + ...userAnalytics ? { userAnalytics } : {}, + ...deprecated !== undefined ? { deprecated } : {}, + ...positional !== undefined ? { positional } : {}, + }; + + options.push(option); + } + + const flattenedSchema = await registry.flatten(schema).toPromise(); + json.schema.visitJsonSchema(flattenedSchema, visitor); + + // Sort by positional. + return options.sort((a, b) => { + if (a.positional) { + if (b.positional) { + return a.positional - b.positional; + } else { + return 1; + } + } else if (b.positional) { + return -1; + } else { + return 0; + } + }); +} diff --git a/packages/angular/cli/utilities/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts new file mode 100644 index 000000000000..09822cef7730 --- /dev/null +++ b/packages/angular/cli/utilities/json-schema_spec.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +import { schema } from '@angular-devkit/core'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { CommandJsonPathException, parseJsonSchemaToCommandDescription } from './json-schema'; + +describe('parseJsonSchemaToCommandDescription', () => { + let registry: schema.CoreSchemaRegistry; + const baseSchemaJson = { + '$schema': 'http://json-schema.org/schema', + '$id': 'ng-cli://commands/version.json', + 'description': 'Outputs Angular CLI version.', + '$longDescription': 'not a file ref', + + '$aliases': ['v'], + '$scope': 'all', + '$impl': './version-impl#VersionCommand', + + 'type': 'object', + 'allOf': [ + { '$ref': './definitions.json#/definitions/base' }, + ], + }; + + beforeEach(() => { + registry = new schema.CoreSchemaRegistry([]); + registry.registerUriHandler((uri: string) => { + if (uri.startsWith('ng-cli://')) { + const content = readFileSync( + join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); + + return Promise.resolve(JSON.parse(content)); + } else { + return null; + } + }); + }); + + it(`should throw on invalid $longDescription path`, async () => { + const name = 'version'; + const schemaPath = join(__dirname, './bad-sample.json'); + const schemaJson = { ...baseSchemaJson, $longDescription: 'not a file ref' }; + try { + await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); + } catch (error) { + const refPath = join(__dirname, schemaJson.$longDescription); + expect(error).toEqual(new CommandJsonPathException(refPath, name)); + + return; + } + expect(true).toBe(false, 'function should have thrown'); + }); + + it(`should throw on invalid $usageNotes path`, async () => { + const name = 'version'; + const schemaPath = join(__dirname, './bad-sample.json'); + const schemaJson = { ...baseSchemaJson, $usageNotes: 'not a file ref' }; + try { + await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); + } catch (error) { + const refPath = join(__dirname, schemaJson.$usageNotes); + expect(error).toEqual(new CommandJsonPathException(refPath, name)); + + return; + } + expect(true).toBe(false, 'function should have thrown'); + }); +}); diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/utilities/package-manager.ts new file mode 100644 index 000000000000..bf948b1dda5f --- /dev/null +++ b/packages/angular/cli/utilities/package-manager.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { getConfiguredPackageManager } from './config'; + +function supports(name: string): boolean { + try { + execSync(`${name} --version`, { stdio: 'ignore' }); + + return true; + } catch { + return false; + } +} + +export function supportsYarn(): boolean { + return supports('yarn'); +} + +export function supportsNpm(): boolean { + return supports('npm'); +} + +export function getPackageManager(root: string): string { + let packageManager = getConfiguredPackageManager(); + if (packageManager) { + return packageManager; + } + + const hasYarn = supportsYarn(); + const hasYarnLock = existsSync(join(root, 'yarn.lock')); + const hasNpm = supportsNpm(); + const hasNpmLock = existsSync(join(root, 'package-lock.json')); + + if (hasYarn && hasYarnLock && !hasNpmLock) { + packageManager = 'yarn'; + } else if (hasNpm && hasNpmLock && !hasYarnLock) { + packageManager = 'npm'; + } else if (hasYarn && !hasNpm) { + packageManager = 'yarn'; + } else if (hasNpm && !hasYarn) { + packageManager = 'npm'; + } + + // TODO: This should eventually inform the user of ambiguous package manager usage. + // Potentially with a prompt to choose and optionally set as the default. + return packageManager || 'npm'; +} diff --git a/packages/angular/cli/utilities/package-metadata.ts b/packages/angular/cli/utilities/package-metadata.ts new file mode 100644 index 000000000000..26b3f446b135 --- /dev/null +++ b/packages/angular/cli/utilities/package-metadata.ts @@ -0,0 +1,240 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging } from '@angular-devkit/core'; +import { existsSync, readFileSync } from 'fs'; +import { homedir } from 'os'; +import * as path from 'path'; + +const ini = require('ini'); +const lockfile = require('@yarnpkg/lockfile'); +const pacote = require('pacote'); + +export interface PackageDependencies { + [dependency: string]: string; +} + +export interface PackageIdentifier { + type: 'git' | 'tag' | 'version' | 'range' | 'file' | 'directory' | 'remote'; + name: string; + scope: string | null; + registry: boolean; + raw: string; + fetchSpec: string; + rawSpec: string; +} + +export interface PackageManifest { + name: string; + version: string; + license?: string; + private?: boolean; + deprecated?: boolean; + + dependencies: PackageDependencies; + devDependencies: PackageDependencies; + peerDependencies: PackageDependencies; + optionalDependencies: PackageDependencies; + + 'ng-add'?: { + + }; + 'ng-update'?: { + migrations: string, + packageGroup: { [name: string]: string }, + }; +} + +export interface PackageMetadata { + name: string; + tags: { [tag: string]: PackageManifest | undefined }; + versions: Map; +} + +let npmrc: { [key: string]: string }; + +function ensureNpmrc(logger: logging.LoggerApi, usingYarn: boolean, verbose: boolean): void { + if (!npmrc) { + try { + npmrc = readOptions(logger, false, verbose); + } catch { } + + if (usingYarn) { + try { + npmrc = { ...npmrc, ...readOptions(logger, true, verbose) }; + } catch { } + } + } +} + +function readOptions( + logger: logging.LoggerApi, + yarn = false, + showPotentials = false, +): Record { + const cwd = process.cwd(); + const baseFilename = yarn ? 'yarnrc' : 'npmrc'; + const dotFilename = '.' + baseFilename; + + let globalPrefix: string; + if (process.env.PREFIX) { + globalPrefix = process.env.PREFIX; + } else { + globalPrefix = path.dirname(process.execPath); + if (process.platform !== 'win32') { + globalPrefix = path.dirname(globalPrefix); + } + } + + const defaultConfigLocations = [ + path.join(globalPrefix, 'etc', baseFilename), + path.join(homedir(), dotFilename), + ]; + + const projectConfigLocations: string[] = [ + path.join(cwd, dotFilename), + ]; + const root = path.parse(cwd).root; + for (let curDir = path.dirname(cwd); curDir && curDir !== root; curDir = path.dirname(curDir)) { + projectConfigLocations.unshift(path.join(curDir, dotFilename)); + } + + if (showPotentials) { + logger.info(`Locating potential ${baseFilename} files:`); + } + + let options: { [key: string]: string } = {}; + for (const location of [...defaultConfigLocations, ...projectConfigLocations]) { + if (existsSync(location)) { + if (showPotentials) { + logger.info(`Trying '${location}'...found.`); + } + + const data = readFileSync(location, 'utf8'); + options = { + ...options, + ...(yarn ? lockfile.parse(data) : ini.parse(data)), + }; + + if (options.cafile) { + const cafile = path.resolve(path.dirname(location), options.cafile); + delete options.cafile; + try { + options.ca = readFileSync(cafile, 'utf8').replace(/\r?\n/, '\\n'); + } catch { } + } + } else if (showPotentials) { + logger.info(`Trying '${location}'...not found.`); + } + } + + // Substitute any environment variable references + for (const key in options) { + if (typeof options[key] === 'string') { + options[key] = options[key].replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || ''); + } + } + + return options; +} + +function normalizeManifest(rawManifest: {}): PackageManifest { + // TODO: Fully normalize and sanitize + + return { + dependencies: {}, + devDependencies: {}, + peerDependencies: {}, + optionalDependencies: {}, + // tslint:disable-next-line:no-any + ...rawManifest as any, + }; +} + +export async function fetchPackageMetadata( + name: string, + logger: logging.LoggerApi, + options?: { + registry?: string; + usingYarn?: boolean; + verbose?: boolean; + }, +): Promise { + const { usingYarn, verbose, registry } = { + registry: undefined, + usingYarn: false, + verbose: false, + ...options, + }; + + ensureNpmrc(logger, usingYarn, verbose); + + const response = await pacote.packument( + name, + { + 'full-metadata': true, + ...npmrc, + ...(registry ? { registry } : {}), + }, + ); + + // Normalize the response + const metadata: PackageMetadata = { + name: response.name, + tags: {}, + versions: new Map(), + }; + + if (response.versions) { + for (const [version, manifest] of Object.entries(response.versions)) { + metadata.versions.set(version, normalizeManifest(manifest as {})); + } + } + + if (response['dist-tags']) { + for (const [tag, version] of Object.entries(response['dist-tags'])) { + const manifest = metadata.versions.get(version as string); + if (manifest) { + metadata.tags[tag] = manifest; + } else if (verbose) { + logger.warn(`Package ${metadata.name} has invalid version metadata for '${tag}'.`); + } + } + } + + return metadata; +} + +export async function fetchPackageManifest( + name: string, + logger: logging.LoggerApi, + options?: { + registry?: string; + usingYarn?: boolean; + verbose?: boolean; + }, +): Promise { + const { usingYarn, verbose, registry } = { + registry: undefined, + usingYarn: false, + verbose: false, + ...options, + }; + + ensureNpmrc(logger, usingYarn, verbose); + + const response = await pacote.manifest( + name, + { + 'full-metadata': true, + ...npmrc, + ...(registry ? { registry } : {}), + }, + ); + + return normalizeManifest(response); +} diff --git a/packages/angular/cli/utilities/package-tree.ts b/packages/angular/cli/utilities/package-tree.ts new file mode 100644 index 000000000000..3005546098eb --- /dev/null +++ b/packages/angular/cli/utilities/package-tree.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface PackageTreeNodeBase { + name: string; + path: string; + realpath: string; + error?: Error; + id: number; + isLink: boolean; + package: { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + 'ng-update'?: { + migrations?: string; + }; + }; + children: PackageTreeNode[]; +} + +export interface PackageTreeActual extends PackageTreeNodeBase { + isLink: false; + parent?: PackageTreeActual; +} + +export interface PackageTreeLink extends PackageTreeNodeBase { + isLink: true; + parent: null; + target: PackageTreeActual; +} + +export type PackageTreeNode = PackageTreeActual | PackageTreeLink; + +export function readPackageTree(path: string): Promise { + const rpt = require('read-package-tree'); + + return new Promise((resolve, reject) => { + rpt(path, (e: Error | undefined, data: PackageTreeNode) => { + if (e) { + reject(e); + } else { + resolve(data); + } + }); + }); +} + +export function findNodeDependencies(root: PackageTreeNode, node = root) { + const actual = node.isLink ? node.target : node; + + const rawDeps: Record = { + ...actual.package.dependencies, + ...actual.package.devDependencies, + ...actual.package.peerDependencies, + }; + + return Object.entries(rawDeps).reduce( + (deps, [name, version]) => { + const depNode = root.children.find(child => { + return child.name === name && !child.isLink && child.parent === node; + }) as PackageTreeActual | undefined; + + deps[name] = depNode || version; + + return deps; + }, + {} as Record, + ); +} diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts index d6becfa363f1..0ec8749d9886 100644 --- a/packages/angular/cli/utilities/project.ts +++ b/packages/angular/cli/utilities/project.ts @@ -11,18 +11,14 @@ import { normalize } from '@angular-devkit/core'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { CommandWorkspace } from '../models/interface'; import { findUp } from './find-up'; -export function insideProject(): boolean { - return getProjectDetails() !== null; +export function insideWorkspace(): boolean { + return getWorkspaceDetails() !== null; } -export interface ProjectDetails { - root: string; - configFile?: string; -} - -export function getProjectDetails(): ProjectDetails | null { +export function getWorkspaceDetails(): CommandWorkspace | null { const currentDir = process.cwd(); const possibleConfigFiles = [ 'angular.json', diff --git a/packages/angular/cli/utilities/tty.ts b/packages/angular/cli/utilities/tty.ts new file mode 100644 index 000000000000..dd5931e26fb6 --- /dev/null +++ b/packages/angular/cli/utilities/tty.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +function _isTruthy(value: undefined | string): boolean { + // Returns true if value is a string that is anything but 0 or false. + return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE'; +} + +export function isTTY(): boolean { + // If we force TTY, we always return true. + const force = process.env['NG_FORCE_TTY']; + if (force !== undefined) { + return _isTruthy(force); + } + + return !!process.stdout.isTTY && !_isTruthy(process.env['CI']); +} diff --git a/packages/angular/pwa/BUILD b/packages/angular/pwa/BUILD index 80cc6f59bedf..62d5bb02bb1b 100644 --- a/packages/angular/pwa/BUILD +++ b/packages/angular/pwa/BUILD @@ -5,7 +5,8 @@ licenses(["notice"]) # MIT -load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") +load("@npm_bazel_typescript//:defs.bzl", "ts_library") +load("//tools:ts_json_schema.bzl", "ts_json_schema") package(default_visibility = ["//visibility:public"]) @@ -24,11 +25,16 @@ ts_library( "**/*_spec_large.ts", ], ), - # Borrow the compile-time deps of the typescript compiler - # Just to avoid an extra npm install action. - node_modules = "@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules", deps = [ + ":pwa_schema", "//packages/angular_devkit/core", "//packages/angular_devkit/schematics", + "@npm//@types/node", + "@npm//rxjs", ], ) + +ts_json_schema( + name = "pwa_schema", + src = "pwa/schema.json", +) diff --git a/packages/angular/pwa/package.json b/packages/angular/pwa/package.json index c419ac9b7b2a..a9ef0a0996b6 100644 --- a/packages/angular/pwa/package.json +++ b/packages/angular/pwa/package.json @@ -2,6 +2,7 @@ "name": "@angular/pwa", "version": "0.0.0", "description": "PWA schematics for Angular", + "experimental": true, "keywords": [ "blueprints", "code generation", @@ -12,6 +13,6 @@ "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0", "@schematics/angular": "0.0.0", - "typescript": "~2.6.2" + "parse5-html-rewriting-stream": "5.1.0" } -} \ No newline at end of file +} diff --git a/packages/angular/pwa/pwa/files/root/manifest.json b/packages/angular/pwa/pwa/files/root/manifest.webmanifest similarity index 100% rename from packages/angular/pwa/pwa/files/root/manifest.json rename to packages/angular/pwa/pwa/files/root/manifest.webmanifest diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts index 53e9f87f7e56..3389528f8743 100644 --- a/packages/angular/pwa/pwa/index.ts +++ b/packages/angular/pwa/pwa/index.ts @@ -1,14 +1,13 @@ /** -* @license -* Copyright Google Inc. All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -import { Path, join, normalize } from '@angular-devkit/core'; + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { getSystemPath, join, normalize } from '@angular-devkit/core'; import { Rule, - SchematicContext, SchematicsException, Tree, apply, @@ -19,153 +18,164 @@ import { template, url, } from '@angular-devkit/schematics'; -import { getWorkspace, getWorkspacePath } from '../utility/config'; +import { Readable, Writable } from 'stream'; import { Schema as PwaOptions } from './schema'; +const RewritingStream = require('parse5-html-rewriting-stream'); -function addServiceWorker(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - context.logger.debug('Adding service worker...'); - - const swOptions = { - ...options, - }; - delete swOptions.title; - - return externalSchematic('@schematics/angular', 'service-worker', swOptions); - }; -} - -function getIndent(text: string): string { - let indent = ''; - - for (const char of text) { - if (char === ' ' || char === '\t') { - indent += char; - } else { - break; - } - } - - return indent; -} - -function updateIndexFile(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; - let path: string; - const projectTargets = project.targets || project.architect; - if (project && projectTargets && projectTargets.build && projectTargets.build.options.index) { - path = projectTargets.build.options.index; - } else { - throw new SchematicsException('Could not find index file for the project'); - } +function updateIndexFile(path: string): Rule { + return (host: Tree) => { const buffer = host.read(path); if (buffer === null) { throw new SchematicsException(`Could not read index file: ${path}`); } - const content = buffer.toString(); - const lines = content.split('\n'); - let closingHeadTagLineIndex = -1; - let closingBodyTagLineIndex = -1; - lines.forEach((line, index) => { - if (closingHeadTagLineIndex === -1 && /<\/head>/.test(line)) { - closingHeadTagLineIndex = index; - } else if (closingBodyTagLineIndex === -1 && /<\/body>/.test(line)) { - closingBodyTagLineIndex = index; - } - }); - const headIndent = getIndent(lines[closingHeadTagLineIndex]) + ' '; - const itemsToAddToHead = [ - '', - '', - ]; + const rewriter = new RewritingStream(); + + let needsNoScript = true; + rewriter.on('startTag', (startTag: { tagName: string }) => { + if (startTag.tagName === 'noscript') { + needsNoScript = false; + } - const bodyIndent = getIndent(lines[closingBodyTagLineIndex]) + ' '; - const itemsToAddToBody = [ - '', - ]; + rewriter.emitStartTag(startTag); + }); - const updatedIndex = [ - ...lines.slice(0, closingHeadTagLineIndex), - ...itemsToAddToHead.map(line => headIndent + line), - ...lines.slice(closingHeadTagLineIndex, closingBodyTagLineIndex), - ...itemsToAddToBody.map(line => bodyIndent + line), - ...lines.slice(closingBodyTagLineIndex), - ].join('\n'); + rewriter.on('endTag', (endTag: { tagName: string }) => { + if (endTag.tagName === 'head') { + rewriter.emitRaw(' \n'); + rewriter.emitRaw(' \n'); + } else if (endTag.tagName === 'body' && needsNoScript) { + rewriter.emitRaw( + ' \n', + ); + } - host.overwrite(path, updatedIndex); + rewriter.emitEndTag(endTag); + }); - return host; + return new Promise(resolve => { + const input = new Readable({ + encoding: 'utf8', + read(): void { + this.push(buffer); + this.push(null); + }, + }); + + const chunks: Array = []; + const output = new Writable({ + write(chunk: string | Buffer, encoding: string, callback: Function): void { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk); + callback(); + }, + final(callback: (error?: Error) => void): void { + const full = Buffer.concat(chunks); + host.overwrite(path, full.toString()); + callback(); + resolve(); + }, + }); + + input.pipe(rewriter).pipe(output); + }); }; } -function addManifestToAssetsConfig(options: PwaOptions) { - return (host: Tree, context: SchematicContext) => { - - const workspacePath = getWorkspacePath(host); - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; - - if (!project) { - throw new Error(`Project is not defined in this workspace.`); +export default function(options: PwaOptions): Rule { + return async host => { + if (!options.title) { + options.title = options.project; } - const assetEntry = join(normalize(project.root), 'src', 'manifest.json'); + // Keep Bazel from failing due to deep import + const { getWorkspace, updateWorkspace } = require('@schematics/angular/utility/workspace'); + + const workspace = await getWorkspace(host); - const projectTargets = project.targets || project.architect; - if (!projectTargets) { - throw new Error(`Targets are not defined for this project.`); + if (!options.project) { + throw new SchematicsException('Option "project" is required.'); } - ['build', 'test'].forEach((target) => { + const project = workspace.projects.get(options.project); + if (!project) { + throw new SchematicsException(`Project is not defined in this workspace.`); + } - const applyTo = projectTargets[target].options; - const assets = applyTo.assets || (applyTo.assets = []); + if (project.extensions['projectType'] !== 'application') { + throw new SchematicsException(`PWA requires a project type of "application".`); + } - assets.push(assetEntry); + // Find all the relevant targets for the project + if (project.targets.size === 0) { + throw new SchematicsException(`Targets are not defined for this project.`); + } - }); + const buildTargets = []; + const testTargets = []; + for (const target of project.targets.values()) { + if (target.builder === '@angular-devkit/build-angular:browser') { + buildTargets.push(target); + } else if (target.builder === '@angular-devkit/build-angular:karma') { + testTargets.push(target); + } + } - host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); + // Add manifest to asset configuration + const assetEntry = join(normalize(project.root), 'src', 'manifest.webmanifest'); + for (const target of [...buildTargets, ...testTargets]) { + if (target.options) { + if (Array.isArray(target.options.assets)) { + target.options.assets.push(assetEntry); + } else { + target.options.assets = [assetEntry]; + } + } else { + target.options = { assets: [assetEntry] }; + } + } - return host; - }; -} + // Find all index.html files in build targets + const indexFiles = new Set(); + for (const target of buildTargets) { + if (target.options && typeof target.options.index === 'string') { + indexFiles.add(target.options.index); + } -export default function (options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const workspace = getWorkspace(host); - if (!options.project) { - throw new SchematicsException('Option "project" is required.'); - } - const project = workspace.projects[options.project]; - if (project.projectType !== 'application') { - throw new SchematicsException(`PWA requires a project type of "application".`); + if (!target.configurations) { + continue; + } + for (const configName in target.configurations) { + const configuration = target.configurations[configName]; + if (configuration && typeof configuration.index === 'string') { + indexFiles.add(configuration.index); + } + } } - const sourcePath = join(project.root as Path, 'src'); + // Setup sources for the assets files to add to the project + const sourcePath = join(normalize(project.root), 'src'); const assetsPath = join(sourcePath, 'assets'); - - options.title = options.title || options.project; - const rootTemplateSource = apply(url('./files/root'), [ template({ ...options }), - move(sourcePath), + move(getSystemPath(sourcePath)), ]); const assetsTemplateSource = apply(url('./files/assets'), [ template({ ...options }), - move(assetsPath), + move(getSystemPath(assetsPath)), ]); + // Setup service worker schematic options + const swOptions = { ...options }; + delete swOptions.title; + + // Chain the rules and return return chain([ - addServiceWorker(options), + updateWorkspace(workspace), + externalSchematic('@schematics/angular', 'service-worker', swOptions), mergeWith(rootTemplateSource), mergeWith(assetsTemplateSource), - updateIndexFile(options), - addManifestToAssetsConfig(options), - ])(host, context); + ...[...indexFiles].map(path => updateIndexFile(path)), + ]); }; } diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts index c7a7dda07933..8f6651c96588 100644 --- a/packages/angular/pwa/pwa/index_spec.ts +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -9,8 +9,6 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/te import * as path from 'path'; import { Schema as PwaOptions } from './schema'; - -// tslint:disable:max-line-length describe('PWA Schematic', () => { const schematicRunner = new SchematicTestRunner( '@angular/pwa', @@ -42,68 +40,106 @@ describe('PWA Schematic', () => { skipTests: false, }; - beforeEach(() => { + beforeEach(async () => { appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'workspace', workspaceOptions); - appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'application', appOptions, appTree); + appTree = await schematicRunner.runExternalSchematicAsync( + '@schematics/angular', + 'application', + appOptions, + appTree, + ).toPromise(); }); - it('should run the service worker schematic', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const configText = tree.readContent('/angular.json'); - const config = JSON.parse(configText); - const swFlag = config.projects.bar.targets.build.configurations.production.serviceWorker; - expect(swFlag).toEqual(true); + it('should run the service worker schematic', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const configText = tree.readContent('/angular.json'); + const config = JSON.parse(configText); + const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker; + expect(swFlag).toEqual(true); + done(); + }, done.fail); }); - it('should create icon files', () => { + it('should create icon files', (done) => { const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; const iconPath = '/projects/bar/src/assets/icons/icon-'; - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - dimensions.forEach(d => { - const path = `${iconPath}${d}x${d}.png`; - expect(tree.exists(path)).toEqual(true); - }); + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + dimensions.forEach(d => { + const path = `${iconPath}${d}x${d}.png`; + expect(tree.exists(path)).toEqual(true); + }); + done(); + }, done.fail); }); - it('should create a manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - expect(tree.exists('/projects/bar/src/manifest.json')).toEqual(true); + it('should create a manifest file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + expect(tree.exists('/projects/bar/src/manifest.webmanifest')).toEqual(true); + done(); + }, done.fail); }); - it('should set the name & short_name in the manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); - const manifest = JSON.parse(manifestText); - expect(manifest.name).toEqual(defaultOptions.title); - expect(manifest.short_name).toEqual(defaultOptions.title); + it('should set the name & short_name in the manifest file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest'); + const manifest = JSON.parse(manifestText); + + expect(manifest.name).toEqual(defaultOptions.title); + expect(manifest.short_name).toEqual(defaultOptions.title); + done(); + }, done.fail); }); - it('should set the name & short_name in the manifest file when no title provided', () => { + it('should set the name & short_name in the manifest file when no title provided', (done) => { const options = {...defaultOptions, title: undefined}; - const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); - const manifest = JSON.parse(manifestText); - expect(manifest.name).toEqual(defaultOptions.project); - expect(manifest.short_name).toEqual(defaultOptions.project); + schematicRunner.runSchematicAsync('ng-add', options, appTree).toPromise().then(tree => { + const manifestText = tree.readContent('/projects/bar/src/manifest.webmanifest'); + const manifest = JSON.parse(manifestText); + + expect(manifest.name).toEqual(defaultOptions.project); + expect(manifest.short_name).toEqual(defaultOptions.project); + done(); + }, done.fail); }); - it('should update the index file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const content = tree.readContent('projects/bar/src/index.html'); + it('should update the index file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const content = tree.readContent('projects/bar/src/index.html'); - expect(content).toMatch(//); - expect(content).toMatch(//); - expect(content) - .toMatch(/