diff --git a/.bazelrc b/.bazelrc index ff817a110e9c..247086af2541 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,3 +1,6 @@ +# Disable NG CLI TTY mode +test --action_env=NG_FORCE_TTY=false + # 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 @@ -89,7 +92,7 @@ test --test_output=errors ################################ # Use the Angular team internal GCP instance for remote execution. -build:remote --remote_instance_name=projects/internal-200822/instances/default_instance +build:remote --remote_instance_name=projects/internal-200822/instances/primary_instance build:remote --project_id=internal-200822 # Starting with Bazel 0.27.0 strategies do not need to be explicitly @@ -99,10 +102,8 @@ 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 +build:remote --remote_timeout=600 +build:remote --jobs=150 # Setup the toolchain and platform for the remote build execution. The platform # is automatically configured by the "rbe_autoconfig" rule in the project workpsace. @@ -117,11 +118,6 @@ 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 @@ -130,15 +126,14 @@ build:remote --remote_accept_cached=true build:remote --cpu=k8 build:remote --host_cpu=k8 +# Set up authentication mechanism for RBE +build:remote --google_default_credentials + ############################### # NodeJS rules settings # These settings are required for rules_nodejs ############################### -# Turn on managed directories feature in Bazel -# This allows us to avoid installing a second copy of node_modules -common --experimental_allow_incremental_repository_updates - #################################################### # User bazel configuration # NOTE: This needs to be the *last* entry in the config. @@ -150,4 +145,4 @@ try-import .bazelrc.user # Enable runfiles even on Windows. # Architect resolves output files from data files, and this isn't possible without runfile support. -test --enable_runfiles \ No newline at end of file +test --enable_runfiles diff --git a/.bazelversion b/.bazelversion index 5a3e0fb5ec48..54f6b9be19dc 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1,3 +1,3 @@ -3.4.1 +4.0.0 # [NB: this comment has to be after the first line, see https://github.com/bazelbuild/bazelisk/issues/117] -# When updating the Bazel version you also need to update the RBE toolchains version in WORKSPACE \ No newline at end of file +# When updating the Bazel version you also need to update the RBE toolchains version in WORKSPACE diff --git a/.circleci/config.yml b/.circleci/config.yml index 56a6364fe701..729fbb635eb5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,8 +17,8 @@ orbs: ## IMPORTANT # Windows needs its own cache key because binaries in node_modules are different. # See https://circleci.com/docs/2.0/caching/#restoring-cache for how prefixes work in CircleCI. -var_1: &cache_key angular_devkit-12.18-{{ checksum "yarn.lock" }} -var_1_win: &cache_key_win angular_devkit-win-12.18-{{ checksum "yarn.lock" }} +var_1: &cache_key v1-angular_devkit-12.18-{{ checksum "yarn.lock" }} +var_1_win: &cache_key_win v1-angular_devkit-win-12.18-{{ checksum "yarn.lock" }} var_3: &default_nodeversion "12.18" # Workspace initially persisted by the `setup` job, and then enhanced by `setup-and-build-win`. # https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs @@ -77,8 +77,8 @@ commands: at: *workspace_location setup_windows: steps: - - run: nvm install 12.1.0 - - run: nvm use 12.1.0 + - run: nvm install 12.13.0 + - run: nvm use 12.13.0 - run: npm install -g yarn@1.17.3 - run: node --version - run: yarn --version @@ -141,7 +141,7 @@ jobs: - restore_cache: keys: - *cache_key - - run: yarn install --frozen-lockfile + - run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - persist_to_workspace: root: *workspace_location paths: @@ -175,10 +175,7 @@ jobs: echo "This build is not over a PR, nothing to do." fi - run: - name: Validate "do not submit" Commits - command: yarn -s admin validate-do-not-submit - - run: - command: yarn -s admin validate --ci + command: yarn -s admin validate e2e-cli: parameters: @@ -198,27 +195,26 @@ jobs: 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.ve >>--ve<> <<# parameters.snapshots >>--ng-snapshots<> --tmpdir=/mnt/ramdisk + command: | + mkdir /mnt/ramdisk/e2e-main + PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.ve >>--ve<> <<# parameters.snapshots >>--ng-snapshots<> --tmpdir=/mnt/ramdisk/e2e-main - run: name: Execute CLI E2E Tests Subset with Yarn command: | - rm -rf /mnt/ramdisk/*test-project - PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.ve >>--ve<> <<# parameters.snapshots >>--ng-snapshots<> --yarn --tmpdir=/mnt/ramdisk --glob="{tests/basic/**,tests/update/**}" + mkdir /mnt/ramdisk/e2e-yarn + PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.ve >>--ve<> <<# parameters.snapshots >>--ng-snapshots<> --yarn --tmpdir=/mnt/ramdisk/e2e-yarn --glob="{tests/basic/**,tests/update/**,tests/commands/add/**}" - e2e-cli-node-10: + e2e-cli-node-14: executor: name: test-executor - nodeversion: "10.20" - parallelism: 4 + nodeversion: "14.15" + parallelism: 6 steps: - custom_attach_workspace - browser-tools/install-chrome - 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 + command: ./.circleci/env.sh - run: PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} test-browsers: @@ -243,7 +239,7 @@ jobs: # 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 --ve - - run: PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e ./tests/legacy-cli/e2e/tests/misc/browsers.ts + - 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: @@ -264,17 +260,6 @@ jobs: command: yarn bazel:test no_output_timeout: 20m - integration: - executor: test-executor - resource_class: xlarge - steps: - - custom_attach_workspace - - install_python - - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc - - run: - command: yarn bazel:integration - no_output_timeout: 20m - snapshot_publish: executor: action-executor resource_class: medium @@ -282,7 +267,7 @@ jobs: - custom_attach_workspace - run: name: Decrypt Credentials - # Note: when changing the image, you might have to re-encrypt the credentials with a + # 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: | @@ -303,13 +288,12 @@ jobs: - restore_cache: keys: - *cache_key_win - - run: yarn install --frozen-lockfile + - run: yarn install --frozen-lockfile --cache-folder ../.cache/yarn - run: yarn build - save_cache: key: *cache_key_win paths: - # Get cache dir on windows via `yarn cache dir` - - C:\Users\circleci\AppData\Local\Yarn\Cache\v4 + - ~/.cache/yarn # Only jobs downstream from this one will see the updated workspace # https://circleci.com/blog/deep-diving-into-circleci-workspaces/ - persist_to_workspace: @@ -324,7 +308,7 @@ jobs: - setup_windows # 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/**,tests/i18n/extract-ivy*.ts}" } - + e2e-cli-win: executor: windows-executor parallelism: 4 @@ -348,10 +332,6 @@ workflows: - build: requires: - setup - filters: - branches: - ignore: - - /docs-preview/ - e2e-cli: post-steps: - store_artifacts: @@ -385,7 +365,7 @@ workflows: only: - renovate/angular - master - - e2e-cli-node-10: + - e2e-cli-node-14: <<: *only_release_branches requires: - e2e-cli @@ -395,15 +375,12 @@ workflows: # Bazel jobs # These jobs only really depend on Setup, but the build job is very quick to run (~35s) and - # will catch any build errors before proceeding to the more lengthy and resource intensive + # will catch any build errors before proceeding to the more lengthy and resource intensive # Bazel jobs. - test: requires: - build - - integration: - requires: - - build - + # Windows jobs # These jobs only run after their non-windows counterparts finish successfully. # This isn't strictly necessary as there is no artifact dependency, but helps economize @@ -422,7 +399,7 @@ workflows: requires: - setup-and-build-win - e2e-cli - + # Publish jobs - snapshot_publish: <<: *only_release_branches @@ -430,3 +407,30 @@ workflows: - build - test - e2e-cli + + nightly: + triggers: + - schedule: + cron: "0 0 * * *" + filters: + branches: + only: + - 11.2.x + jobs: + # Linux jobs + - setup + - build: + requires: + - setup + - e2e-cli: + name: e2e-cli-nightly + requires: + - build + - e2e-cli: + name: e2e-cli-ve-nightly + ve: true + requires: + - build + - test-browsers: + requires: + - build diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.md b/.github/ISSUE_TEMPLATE/1-bug-report.md index 36bb9ef9072b..c24ab5b7ef34 100644 --- a/.github/ISSUE_TEMPLATE/1-bug-report.md +++ b/.github/ISSUE_TEMPLATE/1-bug-report.md @@ -27,7 +27,7 @@ Existing issues often contain information about workarounds, resolution, or prog - [ ] add - [ ] update - [ ] lint -- [ ] xi18n +- [ ] extract-i18n - [ ] run - [ ] config - [ ] help diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.md b/.github/ISSUE_TEMPLATE/2-feature-request.md index 5efe1953908c..9210f5135779 100644 --- a/.github/ISSUE_TEMPLATE/2-feature-request.md +++ b/.github/ISSUE_TEMPLATE/2-feature-request.md @@ -28,7 +28,7 @@ Existing issues often contain information about workarounds, resolution, or prog - [ ] add - [ ] update - [ ] lint -- [ ] xi18n +- [ ] extract-i18n - [ ] run - [ ] config - [ ] help diff --git a/.github/angular-robot.yml b/.github/angular-robot.yml index 6fc65fe348f2..ee4ebcbd2af8 100644 --- a/.github/angular-robot.yml +++ b/.github/angular-robot.yml @@ -14,11 +14,13 @@ merge: failureText: "The following checks are failing:" # comment that will be added to a PR when there is a conflict, leave empty or set to false to disable - mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges. -\nPlease help to unblock it by resolving these conflicts. Thanks!" + mergeConflictComment: >- + Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges. + + Please help to unblock it by resolving these conflicts. Thanks! # label to monitor - mergeLabel: "PR action: merge" + mergeLabel: "action: merge" # list of checks that will determine if the merge label can be added checks: @@ -26,16 +28,15 @@ merge: noConflict: true # whether the PR should have all reviews completed. requireReviews: true - # list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master") + # list of labels that a PR needs to have, checked with a regexp (e.g. "target:" will work for the label "target: major") requiredLabels: - - "PR target: *" + - "target: *" - "cla: yes" # list of labels that a PR shouldn't have, checked after the required labels with a regexp forbiddenLabels: - - "PR target: TBD" - - "PR action: cleanup" - - "PR action: review" + - "action: cleanup" + - "action: review" - "PR state: blocked" - "cla: no" @@ -48,7 +49,6 @@ merge: - "ci/circleci: validate" - "ci/circleci: test" - "ci/circleci: test-win" - - "ci/circleci: integration" - "ci/circleci: e2e-cli" - "ci/circleci: e2e-cli-ve" - "ci/circleci: test-browsers" @@ -58,13 +58,18 @@ merge: # the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable # {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option # {{PLACEHOLDER}} will be replaced by the list of failing checks - mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing: -\n{{PLACEHOLDER}} -\n -\n**If you want your PR to be merged, it has to pass all the CI checks.** -\n -\nIf you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help." + mergeRemovedComment: >- + I see that you just added the `{{MERGE_LABEL}}` label, but the following + checks are still failing: + {{PLACEHOLDER}} + + **If you want your PR to be merged, it has to pass all the CI checks.** + + If you can't get the PR to a green state due to flakes or broken master, + please try rebasing to master and/or restarting the CI job. If that fails + and you believe that the issue is not due to your change, please contact the + caretaker and ask for help. # options for the triage plugin triage: # set to true to disable diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d870a358086..6499bb344192 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,8 +9,8 @@ updates: prefix: "build" labels: - "comp: build & ci" - - "PR target: master & patch" - - "PR action: merge" + - "target: patch" + - "action: merge" # Disable version updates # This does not affect security updates open-pull-requests-limit: 0 diff --git a/.github/workflows/lock-closed.yml b/.github/workflows/lock-closed.yml index 8c154c9c03d4..c8e81beac98a 100644 --- a/.github/workflows/lock-closed.yml +++ b/.github/workflows/lock-closed.yml @@ -9,7 +9,6 @@ jobs: lock_closed: runs-on: ubuntu-latest steps: - - uses: angular/dev-infra/github-actions/lock-closed@414834b2b24dd2df37c6ed00808387ee6fd91b66 - + - uses: angular/dev-infra/github-actions/lock-closed@4f335a4c1f01f20bf905acee2d68c7248f50f2a0 with: lock-bot-key: ${{ secrets.LOCK_BOT_PRIVATE_KEY }} diff --git a/.gitignore b/.gitignore index 80366783e51d..357df0969694 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ yarn-error.log* .ng_pkg_build/ .ng-dev.log .ng-dev.user* +.husky/_ +.bazelrc.user # Mac OSX Finder files. **/.DS_Store diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000000..c9cdc63b0701 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000000..1b07f649c828 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname $0)/_/husky.sh" + +yarn -s ng-dev commit-message pre-commit-validate --file $1; diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 000000000000..3a3afe6f32f5 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname $0)/_/husky.sh" + +yarn -s ng-dev commit-message restore-commit-message-draft $1 $2; diff --git a/.ng-dev/config.ts b/.ng-dev/config.ts index 2e6bd184fdca..a5d99ef26f13 100644 --- a/.ng-dev/config.ts +++ b/.ng-dev/config.ts @@ -1,2 +1,3 @@ export { commitMessage } from './commit-message'; export { github } from './github'; +export { merge } from './merge'; diff --git a/.ng-dev/merge.ts b/.ng-dev/merge.ts new file mode 100644 index 000000000000..eb41a469caad --- /dev/null +++ b/.ng-dev/merge.ts @@ -0,0 +1,26 @@ +import { DevInfraMergeConfig } from '@angular/dev-infra-private/pr/merge/config'; +import { getDefaultTargetLabelConfiguration } from '@angular/dev-infra-private/pr/merge/defaults'; +import { github } from './github'; +import { release } from './release'; + +/** + * Configuration for the merge tool in `ng-dev`. This sets up the labels which + * are respected by the merge script (e.g. the target labels). + */ +export const merge: DevInfraMergeConfig['merge'] = async api => { + return { + githubApiMerge: { + default: 'rebase', + labels: [ + {pattern: 'squash commits', method: 'squash'}, + ], + }, + claSignedLabel: 'cla: yes', + mergeReadyLabel: /^action: merge(-assistance)?/, + caretakerNoteLabel: /(action: merge-assistance)/, + commitMessageFixupLabel: 'commit message fixup', + // We can pick any of the NPM packages as we are in a monorepo where all packages are + // published together with the same version and branching. + labels: await getDefaultTargetLabelConfiguration(api, github, release), + }; +}; diff --git a/.ng-dev/release.ts b/.ng-dev/release.ts new file mode 100644 index 000000000000..175b763add66 --- /dev/null +++ b/.ng-dev/release.ts @@ -0,0 +1,11 @@ +import { ReleaseConfig } from '@angular/dev-infra-private/release/config'; +import { packages } from '../lib/packages'; + +/** Configuration for the `ng-dev release` command. */ +export const release: ReleaseConfig = { + npmPackages: Object.keys(packages), + // TODO: Set up package building. + buildPackages: async () => [], + // TODO: Set up generating changelogs + generateReleaseNotesForHead: async () => {}, +}; diff --git a/.nvmrc b/.nvmrc index 4e31022509a8..fd26bfb7c563 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.13 +10.18.1 diff --git a/.prettierignore b/.prettierignore index fbcd212375a5..04975244c3e8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,9 @@ +/bazel-out/ +/dist-schema/ /etc/api +/tests/ +/README.md +/CONTRIBUTING.md +.yarn/ +dist/ +third_party/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 5e2863a11f68..8ef20e3c1ea0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "printWidth": 100, + "quoteProps": "preserve", "singleQuote": true, "trailingComma": "all" } diff --git a/.yarn/releases/yarn-1.22.5.js b/.yarn/releases/yarn-1.22.10.cjs similarity index 99% rename from .yarn/releases/yarn-1.22.5.js rename to .yarn/releases/yarn-1.22.10.cjs index d31be0b947fb..68b1990b1d92 100755 --- a/.yarn/releases/yarn-1.22.5.js +++ b/.yarn/releases/yarn-1.22.10.cjs @@ -46679,7 +46679,7 @@ function mkdirfix (name, opts, cb) { /* 194 */ /***/ (function(module, exports) { -module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.5","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}} +module.exports = {"name":"yarn","installationMethod":"unknown","version":"1.22.10","license":"BSD-2-Clause","preferGlobal":true,"description":"📦🐈 Fast, reliable, and secure dependency management.","dependencies":{"@zkochan/cmd-shim":"^3.1.0","babel-runtime":"^6.26.0","bytes":"^3.0.0","camelcase":"^4.0.0","chalk":"^2.1.0","cli-table3":"^0.4.0","commander":"^2.9.0","death":"^1.0.0","debug":"^3.0.0","deep-equal":"^1.0.1","detect-indent":"^5.0.0","dnscache":"^1.0.1","glob":"^7.1.1","gunzip-maybe":"^1.4.0","hash-for-dep":"^1.2.3","imports-loader":"^0.8.0","ini":"^1.3.4","inquirer":"^6.2.0","invariant":"^2.2.0","is-builtin-module":"^2.0.0","is-ci":"^1.0.10","is-webpack-bundle":"^1.0.0","js-yaml":"^3.13.1","leven":"^2.0.0","loud-rejection":"^1.2.0","micromatch":"^2.3.11","mkdirp":"^0.5.1","node-emoji":"^1.6.1","normalize-url":"^2.0.0","npm-logical-tree":"^1.2.1","object-path":"^0.11.2","proper-lockfile":"^2.0.0","puka":"^1.0.0","read":"^1.0.7","request":"^2.87.0","request-capture-har":"^1.2.2","rimraf":"^2.5.0","semver":"^5.1.0","ssri":"^5.3.0","strip-ansi":"^4.0.0","strip-bom":"^3.0.0","tar-fs":"^1.16.0","tar-stream":"^1.6.1","uuid":"^3.0.1","v8-compile-cache":"^2.0.0","validate-npm-package-license":"^3.0.4","yn":"^2.0.0"},"devDependencies":{"babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-loader":"^6.2.5","babel-plugin-array-includes":"^2.0.3","babel-plugin-inline-import":"^3.0.0","babel-plugin-transform-builtin-extend":"^1.1.2","babel-plugin-transform-inline-imports-commonjs":"^1.0.0","babel-plugin-transform-runtime":"^6.4.3","babel-preset-env":"^1.6.0","babel-preset-flow":"^6.23.0","babel-preset-stage-0":"^6.0.0","babylon":"^6.5.0","commitizen":"^2.9.6","cz-conventional-changelog":"^2.0.0","eslint":"^4.3.0","eslint-config-fb-strict":"^22.0.0","eslint-plugin-babel":"^5.0.0","eslint-plugin-flowtype":"^2.35.0","eslint-plugin-jasmine":"^2.6.2","eslint-plugin-jest":"^21.0.0","eslint-plugin-jsx-a11y":"^6.0.2","eslint-plugin-prefer-object-spread":"^1.2.1","eslint-plugin-prettier":"^2.1.2","eslint-plugin-react":"^7.1.0","eslint-plugin-relay":"^0.0.28","eslint-plugin-yarn-internal":"file:scripts/eslint-rules","execa":"^0.11.0","fancy-log":"^1.3.2","flow-bin":"^0.66.0","git-release-notes":"^3.0.0","gulp":"^4.0.0","gulp-babel":"^7.0.0","gulp-if":"^2.0.1","gulp-newer":"^1.0.0","gulp-plumber":"^1.0.1","gulp-sourcemaps":"^2.2.0","jest":"^22.4.4","jsinspect":"^0.12.6","minimatch":"^3.0.4","mock-stdin":"^0.3.0","prettier":"^1.5.2","string-replace-loader":"^2.1.1","temp":"^0.8.3","webpack":"^2.1.0-beta.25","yargs":"^6.3.0"},"resolutions":{"sshpk":"^1.14.2"},"engines":{"node":">=4.0.0"},"repository":"yarnpkg/yarn","bin":{"yarn":"./bin/yarn.js","yarnpkg":"./bin/yarn.js"},"scripts":{"build":"gulp build","build-bundle":"node ./scripts/build-webpack.js","build-chocolatey":"powershell ./scripts/build-chocolatey.ps1","build-deb":"./scripts/build-deb.sh","build-dist":"bash ./scripts/build-dist.sh","build-win-installer":"scripts\\build-windows-installer.bat","changelog":"git-release-notes $(git describe --tags --abbrev=0 $(git describe --tags --abbrev=0)^)..$(git describe --tags --abbrev=0) scripts/changelog.md","dupe-check":"yarn jsinspect ./src","lint":"eslint . && flow check","pkg-tests":"yarn --cwd packages/pkg-tests jest yarn.test.js","prettier":"eslint src __tests__ --fix","release-branch":"./scripts/release-branch.sh","test":"yarn lint && yarn test-only","test-only":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --verbose","test-only-debug":"node --inspect-brk --max_old_space_size=4096 node_modules/jest/bin/jest.js --runInBand --verbose","test-coverage":"node --max_old_space_size=4096 node_modules/jest/bin/jest.js --coverage --verbose","watch":"gulp watch","commit":"git-cz"},"jest":{"collectCoverageFrom":["src/**/*.js"],"testEnvironment":"node","modulePathIgnorePatterns":["__tests__/fixtures/","packages/pkg-tests/pkg-tests-fixtures","dist/"],"testPathIgnorePatterns":["__tests__/(fixtures|__mocks__)/","updates/","_(temp|mock|install|init|helpers).js$","packages/pkg-tests"]},"config":{"commitizen":{"path":"./node_modules/cz-conventional-changelog"}}} /***/ }), /* 195 */ @@ -97014,7 +97014,7 @@ let run = exports.run = (() => { if (!(yield (_fs || _load_fs()).exists(lockfilePath))) { yield (_fs || _load_fs()).writeFile(lockfilePath, ''); } - yield (_child || _load_child()).spawn((_constants || _load_constants()).NODE_BIN_PATH, [process.argv[1], 'policies', 'set-version', installVersion], { + yield (_child || _load_child()).spawn((_constants || _load_constants()).NODE_BIN_PATH, [process.argv[1], 'policies', 'set-version', installVersion, '--silent'], { stdio: 'inherit', cwd: config.cwd }); diff --git a/.yarnrc b/.yarnrc index 0a159c3011f5..7fe6e33326b9 100644 --- a/.yarnrc +++ b/.yarnrc @@ -3,4 +3,4 @@ lastUpdateCheck 1581546341989 -yarn-path ".yarn/releases/yarn-1.22.5.js" +yarn-path ".yarn/releases/yarn-1.22.10.cjs" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..8801fb68e07f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ + + +# 11.2.19 (2022-03-30) + +### @angular-devkit/architect-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [75caa1143](https://github.com/angular/angular-cli/commit/75caa1143f4007c9550ab0dabb62ae4df91e3827) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/schematics-cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [80d479e9f](https://github.com/angular/angular-cli/commit/80d479e9fdfcf6863ebbe0986ea6cd29309f398d) | fix | update `minimist` to `1.2.6` | + +### @angular-devkit/benchmark + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------------- | +| [f61cd1a79](https://github.com/angular/angular-cli/commit/f61cd1a79b6960711d4aa5b16d04308bbdc67beb) | fix | update `minimist` to `1.2.6` | + +## Special Thanks + +Alan Agius and Doug Parker + + + + + +# 11.2.16 (2021-12-15) + +### @angular/cli + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------- | +| [f456b0962](https://github.com/angular/angular-cli/commit/f456b0962b9f339759bc86c092256f68d68d9ecf) | fix | error when updating Angular packages across multi-major migrations | +| [886d2511e](https://github.com/angular/angular-cli/commit/886d2511e292b687acce1ac4c6924f992494d14f) | fix | logic which determines which temp version of the CLI is to be download during `ng update` | +| [776d1210a](https://github.com/angular/angular-cli/commit/776d1210a9e62bf2531d977138f49f93820a8b87) | fix | update `ng update` output for Angular packages | + +## Special Thanks + +Alan Agius and Doug Parker diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e09483cc852..817aa82f543f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,7 +117,7 @@ Before you submit your Pull Request (PR) consider the following guidelines: git rebase master -i git push -f ``` - * Add the `PR action: merge` label and the correct + * Add the `action: merge` label and the correct [target label](https://github.com/angular/angular/blob/master/docs/TRIAGE_AND_LABELS.md#pr-target) (if PR author has the project collaborator status, or else the last reviewer should do this). diff --git a/README.md b/README.md index d8179b1d157c..af0d07aa24af 100644 --- a/README.md +++ b/README.md @@ -120,8 +120,7 @@ There are two different test suites which can be run locally: * Run a subset of the tests, use the full Bazel target example: `yarn bazel test //packages/schematics/angular:angular_test` * For a complete list of test targets use the following Bazel query: `yarn bazel query "tests(//packages/...)"` -You can find more info about debugging [tests with Bazel in the docs.] -(https://github.com/angular/angular-cli/blob/master/docs/process/bazel.md#debugging-jasmine_node_test) +You can find more info about debugging [tests with Bazel in the docs.](https://github.com/angular/angular-cli/blob/master/docs/process/bazel.md#debugging-jasmine_node_test) ### End to end tests * Run: `node tests/legacy-cli/run_e2e.js` diff --git a/WORKSPACE b/WORKSPACE index ce17d81e74fd..2d301fdadf24 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -7,8 +7,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "build_bazel_rules_nodejs", - sha256 = "64a71a64ac58b8969bb19b1c9258a973b6433913e958964da698943fb5521d98", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.2.1/rules_nodejs-2.2.1.tar.gz"], + sha256 = "bfacf15161d96a6a39510e7b3d3b522cf61cb8b82a31e79400a84c5abcab5347", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.1/rules_nodejs-3.2.1.tar.gz"], ) # Check the bazel version and download npm dependencies @@ -24,7 +24,7 @@ Angular has a dependency on the @bazel/bazelisk package which supplies it. Try running `yarn bazel` instead. (If you did run that, check that you've got a fresh `yarn install`) """, - minimum_bazel_version = "3.0.0", + minimum_bazel_version = "4.0.0", ) # The NodeJS rules version must be at least the following version because: @@ -62,22 +62,19 @@ yarn_install( "//:tools/yarn/check-yarn.js", ], package_json = "//:package.json", + strict_visibility = False, # Needed for ts-api-guardian. More info about this can be found https://github.com/bazelbuild/rules_nodejs/wiki#strict_visibility-on-yarn_install-and-npm_install-now-defaults-true-2199 yarn_lock = "//:yarn.lock", ) -load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies") - -install_bazel_dependencies(suppress_warning = True) - ########################## # Remote Execution Setup # ########################## # Bring in bazel_toolchains for RBE setup configuration. http_archive( name = "bazel_toolchains", - sha256 = "4fb3ceea08101ec41208e3df9e56ec72b69f3d11c56629d6477c0ff88d711cf7", - strip_prefix = "bazel-toolchains-3.6.0", - url = "https://github.com/bazelbuild/bazel-toolchains/archive/3.6.0.tar.gz", + sha256 = "1adf5db506a7e3c465a26988514cfc3971af6d5b3c2218925cd6e71ee443fc3f", + strip_prefix = "bazel-toolchains-4.0.0", + url = "https://github.com/bazelbuild/bazel-toolchains/archive/4.0.0.tar.gz", ) load("@bazel_toolchains//rules:environments.bzl", "clang_env") @@ -88,7 +85,7 @@ rbe_autoconfig( # Need to specify a base container digest in order to ensure that we can use the checked-in # platform configurations for the "ubuntu16_04" image. Otherwise the autoconfig rule would # need to pull the image and run it in order determine the toolchain configuration. See: - # https://github.com/bazelbuild/bazel-toolchains/blob/3.5.0/configs/ubuntu16_04_clang/versions.bzl + # https://github.com/bazelbuild/bazel-toolchains/blob/4.0.0/configs/ubuntu16_04_clang/versions.bzl base_container_digest = "sha256:f6568d8168b14aafd1b707019927a63c2d37113a03bcee188218f99bd0327ea1", # 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: diff --git a/bin/architect b/bin/architect index 7885feefe26a..f42625548dcc 100755 --- a/bin/architect +++ b/bin/architect @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/bin/benchmark b/bin/benchmark index 4ada663d8bfc..ab9e764db5d2 100755 --- a/bin/benchmark +++ b/bin/benchmark @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/bin/build-optimizer b/bin/build-optimizer index 6f540387043c..edb79fd823ad 100755 --- a/bin/build-optimizer +++ b/bin/build-optimizer @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/bin/devkit-admin b/bin/devkit-admin index 707a854a5b37..ddaf4aff0920 100755 --- a/bin/devkit-admin +++ b/bin/devkit-admin @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/bin/ng b/bin/ng index d43c5512e47b..d3a7c83116d5 100755 --- a/bin/ng +++ b/bin/ng @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/bin/schematics b/bin/schematics index ea3f5176befa..8c3eb81d46c9 100755 --- a/bin/schematics +++ b/bin/schematics @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/docs/design/analytics.md b/docs/design/analytics.md index abdc477910d1..1658f5d4b91a 100644 --- a/docs/design/analytics.md +++ b/docs/design/analytics.md @@ -48,16 +48,16 @@ Note: There's a limit of 20 custom dimensions. | 6 | `--collection` | `string` | | 7 | `Flag: --strict` | `boolean` | | 8 | `Ivy Enabled` | `boolean` | -| 9 | `Flag: --inlineStyle` | `boolean` | -| 10 | `Flag: --inlineTemplate` | `boolean` | -| 11 | `Flag: --viewEncapsulation` | `string` | -| 12 | `Flag: --skipTests` | `boolean` | +| 9 | `Flag: --inline-style` | `boolean` | +| 10 | `Flag: --inline-template` | `boolean` | +| 11 | `Flag: --view-encapsulation` | `string` | +| 12 | `Flag: --skip-tests` | `boolean` | | 13 | `Flag: --aot` | `boolean` | | 14 | `Flag: --minimal` | `boolean` | -| 15 | `Flag: --lintFix` | `boolean` | +| 15 | `Flag: --lint-fix` | `boolean` | | 16 | `Flag: --optimization` | `boolean` | | 17 | `Flag: --routing` | `boolean` | -| 18 | `Flag: --skipImport` | `boolean` | +| 18 | `Flag: --skip-import` | `boolean` | | 19 | `Flag: --export` | `boolean` | | 20 | `Build Errors (comma separated)` | `string` | diff --git a/docs/design/build-system-overview.dot b/docs/design/build-system-overview.dot index bdd50d1e1db6..a337d917364e 100644 --- a/docs/design/build-system-overview.dot +++ b/docs/design/build-system-overview.dot @@ -9,7 +9,7 @@ digraph G { "*.scss\|sass" -> "sass-loader" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; "*.less" -> "less-loader" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; "*.styl" -> "stylus-loader" -> "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer"; - "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer" -> "raw-loader" [label="component style?"] -> "./optimize-css-webpack-plugin.ts"; + "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer" -> "raw-loader, ./optimize-css-webpack-plugin.ts" [label="component style?"]; "raw-loader" -> "./optimize-css-webpack-plugin.ts" "postcss-loader with postcss-import, ./postcss-cli-resources.ts, autoprefixer" -> "style-loader, ./raw-css-loader.ts, and mini-css-extract-plugin" [label="global style?"]; "style-loader, ./raw-css-loader.ts, and mini-css-extract-plugin" -> "./optimize-css-webpack-plugin.ts" diff --git a/docs/process/release.md b/docs/process/release.md index 96d3fde42fae..9863d6b8a39e 100644 --- a/docs/process/release.md +++ b/docs/process/release.md @@ -22,61 +22,18 @@ Alan | Doug Charles | Keen Filipe | Minko -## Triaging Issues -TBD - ## Merging PRs The list of PRs which are currently ready to merge (approved with passing status checks) can be found with [this search](https://github.com/angular/angular-cli/pulls?q=is%3Apr+is%3Aopen+label%3A%22PR+action%3A+merge%22+-is%3Adraft). -This list should be checked daily and any ready PRs should be merged. For each -PR, check the `PR target` label to understand where it should be merged to. If -`master` is targetted, then click "Rebase and Merge". If the PR also targets a -patch branch, see [Maintaining Patch Branches](#maintaining-patch-branches). -Whatever the target, rebasing should be used over merging to avoid cluttering -the Git history with merge commits. - -### Maintaining Patch Branches - -When a PR is merged, if the `PR target` label includes a branch other than -`master`, commits will need to be cherry-picked to an associated branch. In -particular, the `patch` target simply refers to the latest patch branch (eg. -`1.2.x` or `1.3.x-rc.0`). This branch should be updated by cherry-picking all -commits from the PR to it. - -Cherry picking is done by checking out the patch branch and cherry picking the new commit onto it. -The patch branch is simply named as a version number, with a X in the relevant spot, such as `9.0.x`. -This should be done after merging to master. - -```shell -# Make sure commit to upstream/master is present in local repo. -git fetch upstream master - -# Check out patch branch from upstream. -git fetch upstream -git checkout - -# Cherry pick the commit. Use the hash from the commit which was merged -# into upstream/master, which should be known to your local repo. -git cherry-pick -x -# If you have multiple cherry picks, you can do them all here. - -# Resolve merge conflicts if necessary... -# Or abort and ask author to submit a separate commit targeting patch-only. - -# Push changes. -git push upstream -``` - -If you get a `bad revision` error when cherry picking, make sure you are using -the commit hash used when merged into `master`, _not_ the hash listed in the PR. -Also verify that you have fetched `master` from `upstream` since that commit was -merged. +This list should be checked daily and any ready PRs should be merged. For each PR, check the +`target` label to understand where it should be merged to. You can find which branches a specific +PR will be merged into with the `yarn ng-dev pr check-target-branches ` command. -If the commit is not merged to `master` (because it targets `patch only` for -instance), then you will need to fetch the contributor's branch for your local -Git instance to have knowledge of the commit being cherry picked onto the patch -branch. +When ready to merge a PR, run the following command: +``` +yarn ng-dev pr merge +``` ### Maintaining LTS branches @@ -96,27 +53,24 @@ In general, cherry picks for LTS should only be done if it meets one of the crit ## Before releasing -Make sure the CI is green. - -Consider if you need to update [`packages/schematics/angular/utility/latest-versions.ts`](https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/latest-versions.ts) to reflect changes in dependent versions. +Update `Angular` version in [`packages/schematics/angular/utility/latest-versions.ts`](https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/latest-versions.ts). ## Shepparding As commits are cherry-picked when PRs are merged, creating the release should be a matter of creating a tag. -**Make sure you update the package versions in `packages/schematics/angular/utility/latest-versions.ts`.** - ```bash -git commit -a -m 'release: vXX' +git add packages/schematics/angular/utility/latest-versions.ts +git commit -m 'release: vXX' git tag -a 'vXX' -m 'release: tag vXX' ``` -The package versions we are about to publish are derived from the git tag that -we just created. Double check that the versions are correct by running the +The package versions we are about to publish are derived from `version` in the root +[`package.json`](https://github.com/angular/angular-cli/blob/master/package.json#L3). Double check that the versions are correct by running the following command. ```bash -yarn admin packages --version +yarn admin packages --releaseCheck ``` Now push the commit and the tag to the upstream repository. **Make sure to use @@ -131,7 +85,7 @@ git push upstream --follow-tags **This can ONLY be done by a Google employee.** Log in to the Wombat publishing service using your own github and google.com -account to publish. This enforces the loging is done using 2Factor auth. +account to publish. This enforces the login is done using 2Factor auth. Run `npm login --registry https://wombat-dressing-room.appspot.com`: @@ -147,20 +101,20 @@ After closing the tab, you have successfully logged in, it is time to publish. **This can ONLY be done by a Google employee.** -**It is a good idea to wait for CI to be green on the patch branch and tag before doing the release.** +**Wait for CI to be green after pushing the release commit.** For the first release of a major version, follow the instructions in [Publishing a Major Version](#publishing-a-major-version) section. For non-major release, check out the patch branch (e.g. `9.1.x`), then run: ```bash -yarn # Reload dependencies +rm -rf node_modules/ && yarn # Reload dependencies yarn admin publish --tag latest ``` If also publishing a prerelease, check out `master`, then run: ```bash -yarn # Reload dependencies +rm -rf node_modules/ && yarn # Reload dependencies yarn admin publish --tag next ``` @@ -170,7 +124,7 @@ run: **Make sure to update the NPM tag for the version you are releasing!** ```bash -yarn # Reload dependencies +rm -rf node_modules/ && yarn # Reload dependencies yarn admin publish --tag v8-lts ``` @@ -195,30 +149,25 @@ using the `--githubToken` flag. You just then have to confirm the draft. > **Tags containing `next` or `rc` should be marked as pre-release.** -### Microsite Publishing - -The [microsite](https://cli.angular.io/) is the landing page for Angular CLI and -is a one-page static page. +## Post-release Version Update -> **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.** +**For each released version**: -Check out if changes were made to the microsite: +Update the package versions to reflect the *next* release version in **both**: +1. `version` in [`package.json`](https://github.com/angular/angular-cli/blob/master/package.json#L3) +1. `DevkitBuild*` in [`packages/schematics/angular/utility/latest-versions.ts`](https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/latest-versions.ts) ```sh -git log v8.0.0-beta.0..HEAD --oneline etc/cli.angular.io | wc -l +git checkout -b release-bump-vXX +git commit package.json packages/schematics/angular/utility/latest-versions.ts -m "build: bump version to vXX" +git push -u origin release-bump-vXX ``` -If the number is 0 you can ignore the rest of this section. - -To publish, go to the -[`angular-cli/etc/cli.angular.io`](https://github.com/angular/angular-cli/tree/master/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). +Then make a PR and assign it to the next primary caretaker. -This is detailed in [`etc/cli.angular.io/README.md`](https://github.com/angular/angular-cli/blob/master/etc/cli.angular.io/README.md). +Don't forget to update the Slack [#tools](https://angular-team.slack.com/archives/C46U16D4Z) channel topic and also post +links to all the version bump PRs. If anyone else on the team notices these links missing, they should ask the caretaker +to make sure the post-release version bump is not forgotten. ## Publishing a Major Version diff --git a/etc/api/BUILD.bazel b/etc/api/BUILD.bazel index 1818047e8a0c..1d70fb1c9a14 100644 --- a/etc/api/BUILD.bazel +++ b/etc/api/BUILD.bazel @@ -1,4 +1,4 @@ -load("@npm_ts_api_guardian//:index.bzl", "ts_api_guardian_test") +load("@npm//ts-api-guardian:index.bzl", "ts_api_guardian_test") [ts_api_guardian_test( name = "%s_%s_%sapi" % ( diff --git a/etc/api/angular_devkit/core/src/_golden-api.d.ts b/etc/api/angular_devkit/core/src/_golden-api.d.ts index 93168b67c547..0572bee4faef 100644 --- a/etc/api/angular_devkit/core/src/_golden-api.d.ts +++ b/etc/api/angular_devkit/core/src/_golden-api.d.ts @@ -715,6 +715,7 @@ export interface PromptDefinition { }>; message: string; multiselect?: boolean; + propertyTypes: Set; raw?: string | JsonObject; type: string; validator?: (value: JsonValue) => boolean | string | Promise; diff --git a/etc/api/angular_devkit/schematics/tasks/index.d.ts b/etc/api/angular_devkit/schematics/tasks/index.d.ts index 3dc96f5b9e23..1ce44e1880fd 100644 --- a/etc/api/angular_devkit/schematics/tasks/index.d.ts +++ b/etc/api/angular_devkit/schematics/tasks/index.d.ts @@ -4,7 +4,7 @@ export declare class NodePackageInstallTask implements TaskConfigurationGenerato packageName?: string; quiet: boolean; workingDirectory?: string; - constructor(options: Partial); + constructor(options: NodePackageInstallTaskOptions); constructor(workingDirectory?: string); toConfiguration(): TaskConfiguration; } diff --git a/etc/api/angular_devkit/schematics/tools/index.d.ts b/etc/api/angular_devkit/schematics/tools/index.d.ts index 6e8d48b9812f..3573352ab41e 100644 --- a/etc/api/angular_devkit/schematics/tools/index.d.ts +++ b/etc/api/angular_devkit/schematics/tools/index.d.ts @@ -134,7 +134,9 @@ export declare class NodeWorkflow extends workflow.BaseWorkflow { export interface NodeWorkflowOptions { dryRun?: boolean; + engineHostCreator?: (options: NodeWorkflowOptions) => NodeModulesEngineHost; force?: boolean; + optionTransforms?: OptionTransform[]; packageManager?: string; packageRegistry?: string; registry?: schema.CoreSchemaRegistry; diff --git a/etc/cli.angular.io/.firebaserc b/etc/cli.angular.io/.firebaserc deleted file mode 100644 index 3f1a5bf73284..000000000000 --- a/etc/cli.angular.io/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "cli-angular-io" - } -} diff --git a/etc/cli.angular.io/README.md b/etc/cli.angular.io/README.md deleted file mode 100644 index 5e0190d83dc3..000000000000 --- a/etc/cli.angular.io/README.md +++ /dev/null @@ -1,14 +0,0 @@ - -# Angular CLI microsite - -This folder contains all the static files used for the Angular CLI microsite -(http://cli.angular.io). - -To make changes on the frontend, just update the files here, and ask the -caretaker to deploy the new site when the commit is merged. Your commit should -be of scope `docs:` (**NOT** `fix` or `feat`). - -## Deploy - -To deploy, use your firebase credentials to login, then use `firebase deploy` -from this folder. There is currently no build step. diff --git a/etc/cli.angular.io/angular-logo-with-text.svg b/etc/cli.angular.io/angular-logo-with-text.svg deleted file mode 100644 index 51b8487acc9a..000000000000 --- a/etc/cli.angular.io/angular-logo-with-text.svg +++ /dev/null @@ -1,32 +0,0 @@ - \ No newline at end of file diff --git a/etc/cli.angular.io/angular-logo.svg b/etc/cli.angular.io/angular-logo.svg deleted file mode 100644 index fc8b6e94fc32..000000000000 --- a/etc/cli.angular.io/angular-logo.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/etc/cli.angular.io/favicon.ico b/etc/cli.angular.io/favicon.ico deleted file mode 100644 index 4dace951fab2..000000000000 Binary files a/etc/cli.angular.io/favicon.ico and /dev/null differ diff --git a/etc/cli.angular.io/firebase.json b/etc/cli.angular.io/firebase.json deleted file mode 100644 index b55622a85956..000000000000 --- a/etc/cli.angular.io/firebase.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hosting": { - "public": "", - "ignore": [ - "firebase.json", - "README.md", - "**/.*" - ], - "rewrites": [ - { - "source": "/**/!(*.@(js|ts|html|css|json|svg|png|jpg|jpeg))", - "destination": "/index.html" - } - ] - } -} diff --git a/etc/cli.angular.io/index.html b/etc/cli.angular.io/index.html deleted file mode 100644 index 71f55c419027..000000000000 --- a/etc/cli.angular.io/index.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - Angular CLI - - - - - - - - - - -
- -
-
-
- - - - CLI - - - -
- - -
- -
-
-
- - -
-
- - -
-
-
-
-
-
-
-
-
-
- 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 - -
-
-
- - -
- - -
- -
-

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

-

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.

-
- - - -
- - - - -
-
- - - - diff --git a/etc/cli.angular.io/license.html b/etc/cli.angular.io/license.html deleted file mode 100644 index 0131cc303c40..000000000000 --- a/etc/cli.angular.io/license.html +++ /dev/null @@ -1,23 +0,0 @@ -
The MIT License
-
-Copyright (c) Google, Inc. All Rights Reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
- diff --git a/etc/cli.angular.io/main.css b/etc/cli.angular.io/main.css deleted file mode 100644 index 61b0cb066543..000000000000 --- a/etc/cli.angular.io/main.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:"Roboto",Helvetica,sans-serif}h4,h5{font-size:30px;font-weight:400;line-height:40px;margin-bottom:15px;margin-top:15px}h5{font-size:16px;font-weight:300;line-height:28px;margin-bottom:25px;max-width:300px}.mdl-demo section.section--center{max-width:920px}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__header{background-color:#f44336;box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.mdl-layout__header a{color:#fff;text-decoration:none}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:0;width:100%}.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:25px;padding-right:0}.mdl-layout__drawer-button,.top-nav-wrapper label{display:none}@media (max-width:1024px){.mdl-layout__drawer-button{display:inline-block}}.mdl-layout__drawer{margin-top:65px;height:calc(100% - 65px)}@media (max-width:1024px){.mdl-layout__drawer{margin-top:0;height:100%}}.mdl-layout-title,.mdl-layout__title{font-size:16px;line-height:28px;letter-spacing:.02em}.microsite-name{display:inline-block;font-size:20px;margin-left:8px;margin-right:30px;text-transform:uppercase;-webkit-transform:translateY(3px);transform:translateY(3px)}.mdl-navigation__link{font-size:16px;text-transform:uppercase;text-decoration:none}.mdl-navigation__link:hover,.top-nav-wrapper label:hover{background-color:#d32f2f}.top-nav-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media (max-width:800px){.top-nav-wrapper{display:block;position:absolute;right:0;top:0;width:100%}.top-nav-wrapper label{cursor:pointer;display:block;float:right;line-height:56px;padding:0 16px}.top-nav-wrapper nav{background:#d32f2f;clear:both;display:none;height:auto!important}.top-nav-wrapper nav a{display:block}.top-nav-wrapper .mdl-layout-spacer{display:none}input:checked+.top-nav-wrapper label{background:#d32f2f}input:checked+.top-nav-wrapper nav{display:block}}.hero-background{background:-webkit-linear-gradient(#d32f2f ,#f44336);background:linear-gradient(#d32f2f ,#f44336);color:#fff;margin-bottom:60px}.mdl-grid,.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.hero-container{padding:56px 0!important}@media (max-width:830px){.hero-container{text-align:center}}.logo-container{overflow:hidden;text-align:center}@media (max-width:840px){.tagline{max-width:100%}}.mdl-button{height:45px;line-height:45px;min-width:140px;padding:0 30px}.mdl-button--primary.mdl-button--primary.mdl-button--fab,.mdl-button--primary.mdl-button--primary.mdl-button--raised{background-color:#fff;color:#b71c1c}.features-list{width:920px;margin:0 0 23px;padding:15px 200px 15px 15px}@media (max-width:840px){.features-list{padding-right:15px}}.features-list h4{color:#37474f;font-size:28px;font-weight:500;line-height:32px;margin:0 0 16px;opacity:.87}.features-list p,footer ul a{font-size:16px;line-height:30px;opacity:.87}.button-container{margin-bottom:24px!important;text-align:center}.mdl-button--accent.mdl-button--accent.mdl-button--fab,.mdl-button--accent.mdl-button--accent.mdl-button--raised{background-color:#f44336;color:#fff}.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{background-color:#263238;bottom:0;color:#fff;padding-top:0;right:0}footer ul{font-size:14px;font-weight:400;letter-spacing:0;line-height:24px;list-style:none;padding:0}footer ul a{color:#fff;line-height:28px;padding:0;text-decoration:none}footer ul a:hover{text-decoration:underline}@media (max-width:830px){footer ul{background-color:rgba(0,0,0,.12);padding:8px;text-align:center}}.mdl-mega-footer--bottom-section{margin-bottom:0}.mdl-mega-footer--bottom-section p{font-size:12px;margin:0;opacity:.54}.mdl-mega-footer--bottom-section a{color:#fff;font-weight:400;padding:0;text-decoration:none}.power-text{text-align:right}@media (max-width:830px){.power-text{text-align:center;width:calc(100% - 16px)}}.mdl-base{height:100vh} diff --git a/etc/cli.angular.io/material.min.css b/etc/cli.angular.io/material.min.css deleted file mode 100644 index e750f8137205..000000000000 --- a/etc/cli.angular.io/material.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/** - * material-design-lite - Material Design Components in CSS, JS and HTML - * @version v1.1.3 - * @license Apache-2.0 - * @copyright 2015 Google, Inc. - * @link https://github.com/google/material-design-lite - */ -@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%;margin:0}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:#ff4081;font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:#3f51b5 !important}.mdl-color--primary-contrast{background-color:#fff !important}.mdl-color--primary-dark{background-color:#303f9f !important}.mdl-color--accent{background-color:#ff4081 !important}.mdl-color--accent-contrast{background-color:#fff !important}.mdl-color-text--primary{color:#3f51b5 !important}.mdl-color-text--primary-contrast{color:#fff !important}.mdl-color-text--primary-dark{color:#303f9f !important}.mdl-color-text--accent{color:#ff4081 !important}.mdl-color-text--accent-contrast{color:#fff !important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:#ff4081;color:#fff}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:#ff4081;background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:#3f51b5}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:#3f51b5;color:#fff}.mdl-button--raised.mdl-button--colored:hover{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:active{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:#3f51b5}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:#ff4081;color:#fff}.mdl-button--fab.mdl-button--colored:hover{background-color:#ff4081}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:#ff4081}.mdl-button--fab.mdl-button--colored:active{background-color:#ff4081}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:#3f51b5}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:#fff}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:#fff;background-color:#3f51b5}.mdl-button--accent.mdl-button--accent{color:#ff4081}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:#fff}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:#fff;background-color:#ff4081}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:#ff4081;background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid #3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:#3f51b5 url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:#3f51b5}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(63,81,181,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:#3f51b5;z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,#3f51b5 ,#3f51b5)}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:#3f51b5;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#000;color:#e0e0e0}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:50px;font-family:Helvetica,Arial,sans-serif;margin:10px 12px;top:0;left:0;color:#fff;z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:#fff;background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:#3f51b5;color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:#fff;line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:#3f51b5;overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:#3f51b5;color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{display:none;width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:#fff}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:#fff}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#ff4081;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:#fff}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid #3f51b5}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:#3f51b5}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:#3f51b5;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,#3f51b5 16px,#3f51b5 0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:#3f51b5;border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:#3f51b5;border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:#3f51b5;-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:#3f51b5;transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:#3f51b5;transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,#3f51b5 0%,#3f51b5 37.5%,rgba(63,81,181,.26)37.5%,rgba(63,81,181,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:#3f51b5;transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:#3f51b5}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:#ff4081;float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(63,81,181,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:#3f51b5;left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:#3f51b5}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#3f51b5;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:#3f51b5}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:#3f51b5;font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:#3f51b5;bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;will-change:transform;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}} -/*# sourceMappingURL=material.min.css.map */ diff --git a/etc/cli.angular.io/theme.css b/etc/cli.angular.io/theme.css deleted file mode 100644 index b6a336e98b0c..000000000000 --- a/etc/cli.angular.io/theme.css +++ /dev/null @@ -1 +0,0 @@ -.console{width:360px;max-width:92vw;margin-left:15px;margin-right:40px;text-align:left;border-radius:5px;margin-bottom:10px}@media (max-width:830px){.console{margin-right:auto;margin-left:auto}}.console__head{overflow:hidden;background-color:#d5d5d5;padding:8px 15px;border-top-left-radius:5px;border-top-right-radius:5px}.console__dot{float:left;width:12px;height:12px;border-radius:50%;margin-right:7px;box-shadow:0 1px 1px 0 rgba(0,0,0,.2)}.console__dot--red{background-color:#ff6057}.console__dot--yellow{background-color:#ffc22e}.console__dot--green{background-color:#28ca40}.console__body{background-color:#1e1e1e;padding:30px 17px 20px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.console__prompt{display:block;margin-bottom:15px;font-family:"Source Code Pro",monospace;font-size:15px}.console__prompt::before{content:">";padding-right:15px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-base{height:100vh} diff --git a/integration/angular_cli/e2e/tsconfig.json b/integration/angular_cli/e2e/tsconfig.json index c92199cfd63f..a82df00eef37 100644 --- a/integration/angular_cli/e2e/tsconfig.json +++ b/integration/angular_cli/e2e/tsconfig.json @@ -6,7 +6,6 @@ "target": "es2018", "types": [ "jasmine", - "jasminewd2", "node" ] } diff --git a/integration/angular_cli/karma.conf.js b/integration/angular_cli/karma.conf.js index a9c5297de149..646bfde5c8a6 100644 --- a/integration/angular_cli/karma.conf.js +++ b/integration/angular_cli/karma.conf.js @@ -18,6 +18,9 @@ module.exports = function (config) { client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, coverageReporter: { dir: path.join(__dirname, 'coverage'), subdir: '.', diff --git a/integration/angular_cli/package.json b/integration/angular_cli/package.json index feba5be5709a..fb653a1c165a 100644 --- a/integration/angular_cli/package.json +++ b/integration/angular_cli/package.json @@ -8,7 +8,7 @@ "test": "ng test", "lint": "ng lint", "e2e": "ng e2e --prod", - "postinstall": "webdriver-manager update --standalone false --gecko false --versions.chrome 85.0.4183.38" + "postinstall": "webdriver-manager update --standalone false --gecko false --versions.chrome 89.0.4389.0" }, "private": true, "dependencies": { @@ -30,7 +30,6 @@ "@angular/compiler-cli": "~9.1.1", "@types/node": "^12.11.1", "@types/jasmine": "~3.5.0", - "@types/jasminewd2": "~2.0.3", "codelyzer": "^5.1.2", "jasmine-core": "~3.6.0", "jasmine-spec-reporter": "~5.0.0", @@ -40,10 +39,10 @@ "karma-jasmine": "~3.3.0", "karma-jasmine-html-reporter": "^1.5.0", "protractor": "~5.4.3", - "puppeteer": "5.2.1", + "puppeteer": "6.0.0", "ts-node": "~8.3.0", "tslint": "~6.1.0", - "typescript": "~3.9.2" + "typescript": "~4.0.0" }, "resolutions": { "rxjs": "6.5.4", diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js index e491aad25108..b8fbe0962bb8 100644 --- a/lib/bootstrap-local.js +++ b/lib/bootstrap-local.js @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/lib/packages.ts b/lib/packages.ts index dff7a59efbe1..68f76ef586bb 100644 --- a/lib/packages.ts +++ b/lib/packages.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -41,8 +41,12 @@ export interface PackageInfo { export type PackageMap = { [name: string]: PackageInfo }; +export function loadRootPackageJson() { + return require('../package.json'); +} + function loadPackageJson(p: string) { - const root = require('../package.json'); + const root = loadRootPackageJson(); const pkg = require(p); for (const key of Object.keys(root)) { @@ -84,7 +88,7 @@ function loadPackageJson(p: string) { case 'engines': pkg['engines'] = { 'node': '>= 10.13.0', - 'npm': '>= 6.11.0', + 'npm': '^6.11.0 || ^7.5.6', 'yarn': '>= 1.13.0', }; break; @@ -156,25 +160,8 @@ function _getSnapshotHash(_pkg: PackageInfo): string { } -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 = stableToExperimentalVersion(stableVersion); - - return experimental ? experimentalVersion : stableVersion; -} +const stableVersion = loadRootPackageJson().version; +const experimentalVersion = stableToExperimentalVersion(stableVersion); /** * Convert a stable version to its experimental equivalent. For example, @@ -241,9 +228,7 @@ export const packages: PackageMap = dependencies: [], reverseDependencies: [], - get version() { - return _getVersionFromGit(experimental); - }, + version: experimental ? experimentalVersion : stableVersion, }; return packages; diff --git a/lib/registries.ts b/lib/registries.ts index 0bfea087bc2e..2e21b8a4716d 100644 --- a/lib/registries.ts +++ b/lib/registries.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/package.json b/package.json index 57e213e6c2f1..6e635d9486f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "0.0.0", + "version": "11.2.19", "private": true, "description": "Software Development Kit for Angular", "bin": { @@ -33,9 +33,9 @@ "templates": "node ./bin/devkit-admin templates", "validate": "node ./bin/devkit-admin validate", "preinstall": "node ./tools/yarn/check-yarn.js", - "postinstall": "yarn webdriver-update && yarn ngcc", + "postinstall": "yarn webdriver-update && yarn ngcc && yarn husky install", "//webdriver-update-README": "ChromeDriver version must match Puppeteer Chromium version, see https://github.com/GoogleChrome/puppeteer/releases http://chromedriver.chromium.org/downloads", - "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 85.0.4183.38", + "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 89.0.4389.0", "ngcc": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points" }, "repository": { @@ -64,34 +64,35 @@ ] }, "devDependencies": { - "@angular/animations": "11.0.0-next.6", - "@angular/cdk": "10.2.5", - "@angular/common": "11.0.0-next.6", - "@angular/compiler": "11.0.0-next.6", - "@angular/compiler-cli": "11.0.0-next.6", - "@angular/core": "11.0.0-next.6", - "@angular/dev-infra-private": "https://github.com/angular/dev-infra-private-builds.git#d79eccd725b4421e50b566bf001f553b561e3813", - "@angular/forms": "11.0.0-next.6", - "@angular/localize": "11.0.0-next.6", - "@angular/material": "10.2.5", - "@angular/platform-browser": "11.0.0-next.6", - "@angular/platform-browser-dynamic": "11.0.0-next.6", - "@angular/platform-server": "11.0.0-next.6", - "@angular/router": "11.0.0-next.6", - "@angular/service-worker": "11.0.0-next.6", - "@babel/core": "7.11.6", - "@babel/generator": "7.11.6", - "@babel/plugin-transform-runtime": "7.11.5", - "@babel/preset-env": "7.11.5", - "@babel/runtime": "7.11.2", - "@babel/template": "7.10.4", - "@bazel/bazelisk": "1.7.2", - "@bazel/buildifier": "3.5.0", - "@bazel/jasmine": "2.2.1", - "@bazel/typescript": "2.2.1", - "@jsdevtools/coverage-istanbul-loader": "3.0.3", - "@types/babel__core": "7.1.10", - "@types/babel__template": "7.0.3", + "@angular/animations": "11.2.0-next.0", + "@angular/cdk": "11.1.1", + "@angular/common": "11.2.0-next.0", + "@angular/compiler": "11.2.0-next.0", + "@angular/compiler-cli": "11.2.0-next.0", + "@angular/core": "11.2.0-next.0", + "@angular/dev-infra-private": "https://github.com/angular/dev-infra-private-builds.git#0fd0f441648577e8c3966e9b59a88fefe350f514", + "@angular/forms": "11.2.0-next.0", + "@angular/localize": "11.2.0-next.0", + "@angular/material": "11.1.1", + "@angular/platform-browser": "11.2.0-next.0", + "@angular/platform-browser-dynamic": "11.2.0-next.0", + "@angular/platform-server": "11.2.0-next.0", + "@angular/router": "11.2.0-next.0", + "@angular/service-worker": "11.2.0-next.0", + "@babel/core": "7.12.10", + "@babel/generator": "7.12.11", + "@babel/plugin-transform-runtime": "7.12.10", + "@babel/preset-env": "7.12.11", + "@babel/runtime": "7.12.5", + "@babel/template": "7.12.7", + "@bazel/bazelisk": "1.7.3", + "@bazel/buildifier": "4.0.0", + "@bazel/jasmine": "3.2.1", + "@bazel/typescript": "3.2.1", + "@discoveryjs/json-ext": "0.5.2", + "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@types/babel__core": "7.1.12", + "@types/babel__template": "7.4.0", "@types/browserslist": "^4.4.0", "@types/cacache": "^12.0.1", "@types/caniuse-lite": "^1.0.0", @@ -101,10 +102,11 @@ "@types/express": "^4.16.0", "@types/find-cache-dir": "^3.0.0", "@types/glob": "^7.1.1", + "@types/http-proxy": "^1.17.4", "@types/inquirer": "^7.3.0", - "@types/jasmine": "~3.5.0", + "@types/jasmine": "~3.6.0", "@types/karma": "^5.0.0", - "@types/license-checker-webpack-plugin": "^0.0.2", + "@types/license-checker-webpack-plugin": "^0.0.3", "@types/loader-utils": "^2.0.0", "@types/minimatch": "3.0.3", "@types/minimist": "^1.2.0", @@ -128,119 +130,117 @@ "@yarnpkg/lockfile": "1.1.0", "ajv": "6.12.6", "ansi-colors": "4.1.1", - "autoprefixer": "9.8.6", - "babel-loader": "8.1.0", + "autoprefixer": "10.2.4", + "babel-loader": "8.2.2", "bootstrap": "^4.0.0", "browserslist": "^4.9.1", "cacache": "15.0.5", "caniuse-lite": "^1.0.30001032", - "circular-dependency-plugin": "5.2.0", + "circular-dependency-plugin": "5.2.2", "codelyzer": "^6.0.0", "common-tags": "^1.8.0", "conventional-changelog": "^3.0.0", "conventional-commits-parser": "^3.0.0", - "copy-webpack-plugin": "6.2.1", - "core-js": "3.6.5", - "css-loader": "5.0.0", - "cssnano": "4.1.10", + "copy-webpack-plugin": "6.3.2", + "core-js": "3.8.3", + "critters": "0.0.12", + "css-loader": "5.0.1", + "cssnano": "5.0.2", "debug": "^4.1.1", - "enhanced-resolve": "5.2.0", + "enhanced-resolve": "5.7.0", "express": "4.17.1", "fast-json-stable-stringify": "2.1.0", - "file-loader": "6.1.1", + "file-loader": "6.2.0", "find-cache-dir": "3.3.1", "font-awesome": "^4.7.0", "gh-got": "^9.0.0", "git-raw-commits": "^2.0.0", "glob": "7.1.6", - "husky": "^4.0.10", + "http-proxy": "^1.18.1", + "https-proxy-agent": "5.0.0", + "husky": "5.0.8", "inquirer": "7.3.3", "jasmine": "^3.3.1", "jasmine-core": "~3.6.0", "jasmine-spec-reporter": "~6.0.0", - "jest-worker": "26.5.0", + "jest-worker": "26.6.2", "jquery": "^3.3.1", - "jsonc-parser": "2.3.1", - "karma": "~5.2.0", + "jsonc-parser": "3.0.0", + "karma": "~6.1.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-source-map-support": "1.4.0", - "less": "3.12.2", - "less-loader": "7.0.2", + "less": "4.1.1", + "less-loader": "7.3.0", "license-checker": "^25.0.0", - "license-checker-webpack-plugin": "0.1.5", + "license-checker-webpack-plugin": "0.2.1", "loader-utils": "2.0.0", - "mini-css-extract-plugin": "1.0.0", + "magic-string": "0.25.7", + "mini-css-extract-plugin": "1.3.5", "minimatch": "3.0.4", - "minimist": "^1.2.0", - "ng-packagr": "~11.0.0-next.0", + "minimist": "1.2.6", + "ng-packagr": "~11.1.0", "node-fetch": "^2.2.0", "npm-registry-client": "8.6.0", - "open": "7.3.0", - "ora": "5.1.0", - "pacote": "11.1.4", + "open": "7.4.0", + "ora": "5.3.0", + "pacote": "11.2.4", "parse5-html-rewriting-stream": "6.0.1", "pidtree": "^0.5.0", "pidusage": "^2.0.17", "pnp-webpack-plugin": "1.6.4", "popper.js": "^1.14.1", - "postcss": "7.0.32", - "postcss-import": "12.0.1", - "postcss-loader": "4.0.4", + "postcss": "8.2.15", + "postcss-import": "14.0.0", + "postcss-loader": "4.2.0", "prettier": "^2.0.0", "protractor": "~7.0.0", - "puppeteer": "5.3.1", - "quicktype-core": "^6.0.15", + "puppeteer": "6.0.0", + "quicktype-core": "^6.0.69", "raw-loader": "4.0.2", "regenerator-runtime": "0.13.7", - "resolve-url-loader": "3.1.1", + "resolve-url-loader": "4.0.0", "rimraf": "3.0.2", - "rollup": "2.30.0", + "rollup": "2.38.4", "rxjs": "6.6.3", - "sass": "1.27.0", - "sass-loader": "10.0.3", - "sauce-connect-proxy": "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz", - "semver": "7.3.2", + "sass": "1.32.6", + "sass-loader": "10.1.1", + "sauce-connect-proxy": "https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz", + "semver": "7.3.4", "source-map": "0.7.3", - "source-map-loader": "1.1.1", - "source-map-support": "0.5.16", + "source-map-loader": "1.1.3", + "source-map-support": "0.5.19", "spdx-satisfies": "^5.0.0", - "speed-measure-webpack-plugin": "1.3.3", + "speed-measure-webpack-plugin": "1.4.2", "style-loader": "2.0.0", - "stylus": "0.54.7", - "stylus-loader": "4.1.1", - "symbol-observable": "2.0.3", + "stylus": "0.54.8", + "stylus-loader": "4.3.3", + "symbol-observable": "3.0.0", "tar": "^6.0.0", "temp": "^0.9.0", - "terser": "5.3.5", + "terser": "5.5.1", "terser-webpack-plugin": "4.2.3", "text-table": "0.2.0", "through2": "^4.0.0", "tree-kill": "1.2.2", - "ts-api-guardian": "0.5.0", + "ts-api-guardian": "0.6.0", "ts-node": "^5.0.0", "tslib": "^2.0.0", "tslint": "^6.1.3", "tslint-no-circular-imports": "^0.7.0", "tslint-sonarts": "1.9.0", - "typescript": "4.0.3", - "verdaccio": "4.8.1", + "typescript": "4.1.5", + "verdaccio": "4.11.0", "verdaccio-auth-memory": "^9.7.2", "webpack": "4.44.2", "webpack-dev-middleware": "3.7.2", - "webpack-dev-server": "3.10.3", - "webpack-merge": "5.2.0", - "webpack-sources": "2.0.1", - "webpack-subresource-integrity": "1.5.0", + "webpack-dev-server": "3.11.3", + "webpack-merge": "5.7.3", + "webpack-sources": "2.2.0", + "webpack-subresource-integrity": "1.5.2", "worker-plugin": "5.0.0", - "zone.js": "^0.10.2" - }, - "husky": { - "hooks": { - "commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS", - "prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS" - } + "zone.js": "^0.11.3" } } diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 8395b2a215eb..9e20fc349a84 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -46,7 +46,7 @@ ts_library( "//packages/angular/cli:commands/update.ts", "//packages/angular/cli:commands/version.ts", "//packages/angular/cli:commands/run.ts", - "//packages/angular/cli:commands/xi18n.ts", + "//packages/angular/cli:commands/extract-i18n.ts", # @external_end ], data = glob( @@ -69,6 +69,7 @@ ts_library( "//packages/angular_devkit/core/node", "//packages/angular_devkit/schematics", "//packages/angular_devkit/schematics/tools", + "@npm//@angular/core", "@npm//@types/debug", "@npm//@types/inquirer", "@npm//@types/node", @@ -78,6 +79,9 @@ ts_library( "@npm//@types/universal-analytics", "@npm//@types/uuid", "@npm//ansi-colors", + "@npm//jsonc-parser", + "@npm//open", + "@npm//ora", ], ) @@ -223,8 +227,8 @@ ts_json_schema( ) ts_json_schema( - name = "xi18n_schema", - src = "commands/xi18n.json", + name = "extract-i18n_schema", + src = "commands/extract-i18n.json", data = [ "commands/definitions.json", ], diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md index c4c5e25a8440..96b40aacbad5 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -250,7 +250,7 @@ In addition to this one, another, more elaborated way to capture a CPU profile u ## Documentation -The documentation for the Angular CLI is located in this repo's [wiki](https://angular.io/cli). +The documentation for the Angular CLI is located on our [documentation website](https://angular.io/cli). ## License diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json index 85de6cc170d3..0b65947a0647 100644 --- a/packages/angular/cli/commands.json +++ b/packages/angular/cli/commands.json @@ -6,6 +6,7 @@ "deploy": "./commands/deploy.json", "doc": "./commands/doc.json", "e2e": "./commands/e2e.json", + "extract-i18n": "./commands/extract-i18n.json", "make-this-awesome": "./commands/easter-egg.json", "generate": "./commands/generate.json", "help": "./commands/help.json", @@ -15,6 +16,5 @@ "serve": "./commands/serve.json", "test": "./commands/test.json", "update": "./commands/update.json", - "version": "./commands/version.json", - "xi18n": "./commands/xi18n.json" + "version": "./commands/version.json" } diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index 321601e22e22..a58f7ec33e95 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -13,15 +13,16 @@ import { PackageManager } from '../lib/config/schema'; import { isPackageNameSafeForAnalytics } from '../models/analytics'; import { Arguments } from '../models/interface'; import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command'; -import { installPackage, installTempPackage } from '../tasks/install-package'; import { colors } from '../utilities/color'; -import { getPackageManager } from '../utilities/package-manager'; +import { installPackage, installTempPackage } from '../utilities/install-package'; +import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; import { NgAddSaveDepedency, PackageManifest, fetchPackageManifest, fetchPackageMetadata, } from '../utilities/package-metadata'; +import { Spinner } from '../utilities/spinner'; import { Schema as AddCommandSchema } from './add'; const npa = require('npm-package-arg'); @@ -38,6 +39,8 @@ export class AddCommand extends SchematicCommand { } async run(options: AddCommandSchema & Arguments) { + await ensureCompatibleNpm(this.context.root); + if (!options.collection) { this.logger.fatal( `The "ng add" command requires a name argument to be specified eg. ` + @@ -79,12 +82,18 @@ export class AddCommand extends SchematicCommand { } } + const spinner = new Spinner(); + + spinner.start('Determining package manager...'); const packageManager = await getPackageManager(this.context.root); const usingYarn = packageManager === PackageManager.Yarn; + spinner.info(`Using package manager: ${colors.grey(packageManager)}`); if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) { // only package name provided; search for viable version // plus special cases for packages that did not have peer deps setup + spinner.start('Searching for compatible package version...'); + let packageMetadata; try { packageMetadata = await fetchPackageMetadata(packageIdentifier.name, this.logger, { @@ -93,7 +102,7 @@ export class AddCommand extends SchematicCommand { verbose: options.verbose, }); } catch (e) { - this.logger.error('Unable to fetch package metadata: ' + e.message); + spinner.fail('Unable to load package information from registry: ' + e.message); return 1; } @@ -111,11 +120,14 @@ export class AddCommand extends SchematicCommand { ) { packageIdentifier = npa.resolve('@angular/pwa', '0.12'); } + } else { + packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); } + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) { // 'latest' is invalid so search for most recent matching package const versionManifests = Object.values(packageMetadata.versions).filter( - (value: PackageManifest) => !prerelease(value.version), + (value: PackageManifest) => !prerelease(value.version) && !value.deprecated, ) as PackageManifest[]; versionManifests.sort((a, b) => rcompare(a.version, b.version, true)); @@ -129,10 +141,14 @@ export class AddCommand extends SchematicCommand { } if (!newIdentifier) { - this.logger.warn("Unable to find compatible package. Using 'latest'."); + spinner.warn("Unable to find compatible package. Using 'latest'."); } else { packageIdentifier = newIdentifier; + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } + } else { + packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } } @@ -140,6 +156,7 @@ export class AddCommand extends SchematicCommand { let savePackage: NgAddSaveDepedency | undefined; try { + spinner.start('Loading package information from registry...'); const manifest = await fetchPackageManifest(packageIdentifier, this.logger, { registry: options.registry, verbose: options.verbose, @@ -150,41 +167,51 @@ export class AddCommand extends SchematicCommand { collectionName = manifest.name; if (await this.hasMismatchedPeer(manifest)) { - this.logger.warn( + spinner.warn( 'Package has unmet peer dependencies. Adding the package may not succeed.', ); + } else { + spinner.succeed(`Package information loaded.`); } } catch (e) { - this.logger.error('Unable to fetch package manifest: ' + e.message); + spinner.fail(`Unable to fetch package information for '${packageIdentifier}': ${e.message}`); return 1; } - if (savePackage === false) { - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const tempPath = installTempPackage( - packageIdentifier.raw, - this.logger, - packageManager, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); - const resolvedCollectionPath = require.resolve( - join(collectionName, 'package.json'), - { - paths: [tempPath], - }, - ); + try { + spinner.start('Installing package...'); + if (savePackage === false) { + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const tempPath = installTempPackage( + packageIdentifier.raw, + undefined, + packageManager, + options.registry ? [`--registry="${options.registry}"`] : undefined, + ); + const resolvedCollectionPath = require.resolve( + join(collectionName, 'package.json'), + { + paths: [tempPath], + }, + ); - collectionName = dirname(resolvedCollectionPath); - } else { - installPackage( - packageIdentifier.raw, - this.logger, - packageManager, - savePackage, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); + collectionName = dirname(resolvedCollectionPath); + } else { + installPackage( + packageIdentifier.raw, + undefined, + packageManager, + savePackage, + options.registry ? [`--registry="${options.registry}"`] : undefined, + ); + } + spinner.succeed('Package successfully installed.'); + } catch (error) { + spinner.fail(`Package installation failed: ${error.message}`); + + return 1; } return this.executeSchematic(collectionName, options['--']); diff --git a/packages/angular/cli/commands/analytics-impl.ts b/packages/angular/cli/commands/analytics-impl.ts index 9174005d7b18..9897437d50c0 100644 --- a/packages/angular/cli/commands/analytics-impl.ts +++ b/packages/angular/cli/commands/analytics-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts index 0bdd50b57949..e0336279a456 100644 --- a/packages/angular/cli/commands/build-impl.ts +++ b/packages/angular/cli/commands/build-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build-long.md index 4e10498f4cd5..a28a5f56d532 100644 --- a/packages/angular/cli/commands/build-long.md +++ b/packages/angular/cli/commands/build-long.md @@ -3,7 +3,7 @@ When used to build a library, a different builder is invoked, and only the `ts-c 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. +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` 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. diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts index 944ca1df100a..3144f23f7bd9 100644 --- a/packages/angular/cli/commands/config-impl.ts +++ b/packages/angular/cli/commands/config-impl.ts @@ -1,21 +1,12 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { - InvalidJsonCharacterException, - JsonArray, - JsonObject, - JsonParseMode, - JsonValue, - parseJson, - tags, -} from '@angular-devkit/core'; -import { writeFileSync } from 'fs'; +import { JsonValue, tags } from '@angular-devkit/core'; import { v4 as uuidV4 } from 'uuid'; import { Command } from '../models/command'; import { Arguments, CommandScope } from '../models/interface'; @@ -24,50 +15,16 @@ import { migrateLegacyGlobalConfig, validateWorkspace, } from '../utilities/config'; -import { Schema as ConfigCommandSchema, Value as ConfigCommandSchemaValue } from './config'; - -function _validateBoolean(value: string) { - if (('' + value).trim() === 'true') { - return true; - } else if (('' + value).trim() === 'false') { - return false; - } else { - throw new Error(`Invalid value type; expected Boolean, received ${JSON.stringify(value)}.`); - } -} -function _validateString(value: string) { - return value; -} -function _validateAnalytics(value: string) { - if (value === '') { - // Disable analytics. - return null; - } else { - return value; - } -} -function _validateAnalyticsSharingUuid(value: string) { - if (value == '') { - return uuidV4(); - } else { - return value; - } -} -function _validateAnalyticsSharingTracking(value: string) { - if (!value.match(/^GA-\d+-\d+$/)) { - throw new Error(`Invalid GA property ID: ${JSON.stringify(value)}.`); - } - - return value; -} - -const validCliPaths = new Map JsonValue>([ - ['cli.warnings.versionMismatch', _validateBoolean], - ['cli.defaultCollection', _validateString], - ['cli.packageManager', _validateString], - ['cli.analytics', _validateAnalytics], - ['cli.analyticsSharing.tracking', _validateAnalyticsSharingTracking], - ['cli.analyticsSharing.uuid', _validateAnalyticsSharingUuid], +import { JSONFile, parseJson } from '../utilities/json-file'; +import { Schema as ConfigCommandSchema } from './config'; + +const validCliPaths = new Map string) | undefined>([ + ['cli.warnings.versionMismatch', undefined], + ['cli.defaultCollection', undefined], + ['cli.packageManager', undefined], + ['cli.analytics', undefined], + ['cli.analyticsSharing.tracking', undefined], + ['cli.analyticsSharing.uuid', v => v ? `${v}` : uuidV4()], ]); /** @@ -106,92 +63,17 @@ function parseJsonPath(path: string): (string | number)[] { return result.filter(fragment => fragment != null); } -function getValueFromPath( - root: T, - path: string, -): JsonValue | undefined { - const fragments = parseJsonPath(path); - - try { - return fragments.reduce((value: JsonValue | undefined, current: string | number) => { - if (value == undefined || typeof value != 'object') { - return undefined; - } else if (typeof current == 'string' && !Array.isArray(value)) { - return value[current]; - } else if (typeof current == 'number' && Array.isArray(value)) { - return value[current]; - } else { - return undefined; - } - }, root); - } catch { - return undefined; - } -} - -function setValueFromPath( - root: T, - path: string, - newValue: JsonValue, -): JsonValue | undefined { - const fragments = parseJsonPath(path); - - try { - return fragments.reduce((value: JsonValue | undefined, current: string | number, index: number) => { - if (value == undefined || typeof value != 'object') { - return undefined; - } else if (typeof current == 'string' && !Array.isArray(value)) { - if (index === fragments.length - 1) { - value[current] = newValue; - } else if (value[current] == undefined) { - if (typeof fragments[index + 1] == 'number') { - value[current] = []; - } else if (typeof fragments[index + 1] == 'string') { - value[current] = {}; - } - } - - return value[current]; - } else if (typeof current == 'number' && Array.isArray(value)) { - if (index === fragments.length - 1) { - value[current] = newValue; - } else if (value[current] == undefined) { - if (typeof fragments[index + 1] == 'number') { - value[current] = []; - } else if (typeof fragments[index + 1] == 'string') { - value[current] = {}; - } - } - - return value[current]; - } else { - return undefined; - } - }, root); - } catch { - return undefined; - } -} - -function normalizeValue(value: ConfigCommandSchemaValue, path: string): JsonValue { - const cliOptionType = validCliPaths.get(path); - if (cliOptionType) { - return cliOptionType('' + value); - } - - if (typeof value === 'string') { - try { - return parseJson(value, JsonParseMode.Loose); - } catch (e) { - if (e instanceof InvalidJsonCharacterException && !value.startsWith('{')) { - return value; - } else { - throw e; - } - } +function normalizeValue(value: string | undefined | boolean | number): JsonValue | undefined { + const valueString = `${value}`.trim(); + if (valueString === 'true') { + return true; + } else if (valueString === 'false') { + return false; + } else if (isFinite(+valueString)) { + return +valueString; } - return value; + return value || undefined; } export class ConfigCommand extends Command { @@ -212,7 +94,7 @@ export class ConfigCommand extends Command { We found a global configuration that was used in Angular CLI 1. It has been automatically migrated.`); } - } catch {} + } catch { } } if (options.value == undefined) { @@ -222,35 +104,35 @@ export class ConfigCommand extends Command { return 1; } - return this.get(config.value, options); + return this.get(config, options); } else { return this.set(options); } } - private get(config: JsonObject, options: ConfigCommandSchema) { + private get(jsonFile: JSONFile, options: ConfigCommandSchema) { let value; if (options.jsonPath) { - value = getValueFromPath(config, options.jsonPath); + value = jsonFile.get(parseJsonPath(options.jsonPath)); } else { - value = config; + value = jsonFile.content; } if (value === undefined) { this.logger.error('Value cannot be found.'); return 1; - } else if (typeof value == 'object') { - this.logger.info(JSON.stringify(value, null, 2)); + } else if (typeof value === 'string') { + this.logger.info(value); } else { - this.logger.info(value.toString()); + this.logger.info(JSON.stringify(value, null, 2)); } return 0; } private async set(options: ConfigCommandSchema) { - if (!options.jsonPath || !options.jsonPath.trim()) { + if (!options.jsonPath?.trim()) { throw new Error('Invalid Path.'); } @@ -269,28 +151,25 @@ export class ConfigCommand extends Command { return 1; } - // TODO: Modify & save without destroying comments - const configValue = config.value; - - const value = normalizeValue(options.value || '', options.jsonPath); - const result = setValueFromPath(configValue, options.jsonPath, value); + const jsonPath = parseJsonPath(options.jsonPath); + const value = validCliPaths.get(options.jsonPath)?.(options.value) ?? options.value; + const modified = config.modify(jsonPath, normalizeValue(value)); - if (result === undefined) { + if (!modified) { this.logger.error('Value cannot be found.'); return 1; } try { - await validateWorkspace(configValue); + await validateWorkspace(parseJson(config.content)); } catch (error) { this.logger.fatal(error.message); return 1; } - const output = JSON.stringify(configValue, null, 2); - writeFileSync(configPath, output); + config.save(); return 0; } diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json index d0c3d59520b3..21a289f269d6 100644 --- a/packages/angular/cli/commands/config.json +++ b/packages/angular/cli/commands/config.json @@ -31,7 +31,7 @@ }, "global": { "type": "boolean", - "description": "When true, accesses the global configuration in the caller's home directory.", + "description": "Access the global configuration in the caller's home directory.", "default": false, "aliases": ["g"] } diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json index 1713520fc1d1..222a790bf09e 100644 --- a/packages/angular/cli/commands/definitions.json +++ b/packages/angular/cli/commands/definitions.json @@ -14,14 +14,14 @@ } }, "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", + "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.\nSetting this explicitly overrides the \"--prod\" flag.", "type": "string", "aliases": [ "c" ] }, "prod": { - "description": "Shorthand for \"--configuration=production\".\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.", + "description": "Shorthand for \"--configuration=production\".\nSet the build configuration to the production target.\nBy default, the production target is set up in the workspace configuration such that all builds make use of bundling, limited tree-shaking, and also limited dead code elimination.", "type": "boolean" } } @@ -42,13 +42,13 @@ "type": "boolean", "default": false, "aliases": [ "d" ], - "description": "When true, runs through and reports activity without writing out results." + "description": "Run through and reports activity without writing out results." }, "force": { "type": "boolean", "default": false, "aliases": [ "f" ], - "description": "When true, forces overwriting of existing files." + "description": "Force overwriting of existing files." } } }, @@ -57,12 +57,12 @@ "interactive": { "type": "boolean", "default": "true", - "description": "When false, disables interactive input prompts." + "description": "Enable interactive input prompts." }, "defaults": { "type": "boolean", "default": "false", - "description": "When true, disables interactive input prompts for options with a default." + "description": "Disable interactive input prompts for options with a default." } } } diff --git a/packages/angular/cli/commands/deploy-impl.ts b/packages/angular/cli/commands/deploy-impl.ts index 9ce45a22ed28..819b63f61986 100644 --- a/packages/angular/cli/commands/deploy-impl.ts +++ b/packages/angular/cli/commands/deploy-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/deploy-long.md b/packages/angular/cli/commands/deploy-long.md index 5d600ef2e8d8..afe619565c0a 100644 --- a/packages/angular/cli/commands/deploy-long.md +++ b/packages/angular/cli/commands/deploy-long.md @@ -8,15 +8,15 @@ For example: ```json "projects": { - "my-project": { - ... - "architect": { - ... - "deploy": { - "builder": "@angular/fire:deploy", - "options": {} - } - } - } + "my-project": { + ... + "architect": { + ... + "deploy": { + "builder": "@angular/fire:deploy", + "options": {} + } } + } +} ``` \ No newline at end of file diff --git a/packages/angular/cli/commands/deploy.json b/packages/angular/cli/commands/deploy.json index 9bfc66786ca4..30e84ab4e0ac 100644 --- a/packages/angular/cli/commands/deploy.json +++ b/packages/angular/cli/commands/deploy.json @@ -20,7 +20,7 @@ } }, "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.", + "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", "type": "string", "aliases": [ "c" diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts index 587d760c3346..6369fe2fd054 100644 --- a/packages/angular/cli/commands/doc-impl.ts +++ b/packages/angular/cli/commands/doc-impl.ts @@ -1,17 +1,16 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import * as open from 'open'; import { Command } from '../models/command'; import { Arguments } from '../models/interface'; import { Schema as DocCommandSchema } from './doc'; -const open = require('open'); - export class DocCommand extends Command { public async run(options: DocCommandSchema & Arguments) { if (!options.keyword) { @@ -39,7 +38,7 @@ export class DocCommand extends Command { // and use it if we can find it try { /* tslint:disable-next-line:no-implicit-dependencies */ - const currentNgVersion = require('@angular/core').VERSION.major; + const currentNgVersion = (await import('@angular/core')).VERSION.major; domain = `v${currentNgVersion}.angular.io`; } catch (e) { } } @@ -50,11 +49,8 @@ export class DocCommand extends Command { searchUrl = `https://${domain}/docs?search=${options.keyword}`; } - // We should wrap `open` in a new Promise because `open` is already resolved - await new Promise(() => { - open(searchUrl, { - wait: false, - }); + await open(searchUrl, { + wait: false, }); } } diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json index b43f448d03cb..eb8c31d935bd 100644 --- a/packages/angular/cli/commands/doc.json +++ b/packages/angular/cli/commands/doc.json @@ -24,7 +24,7 @@ "aliases": ["s"], "type": "boolean", "default": false, - "description": "When true, searches all of angular.io. Otherwise, searches only API reference documentation." + "description": "Search all of angular.io. Otherwise, searches only API reference documentation." }, "version" : { "oneOf": [ diff --git a/packages/angular/cli/commands/e2e-impl.ts b/packages/angular/cli/commands/e2e-impl.ts index f24a44ca4122..1fe6a373ba2c 100644 --- a/packages/angular/cli/commands/e2e-impl.ts +++ b/packages/angular/cli/commands/e2e-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts index 8e70d46dc827..22e43d6fe996 100644 --- a/packages/angular/cli/commands/easter-egg-impl.ts +++ b/packages/angular/cli/commands/easter-egg-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/xi18n-impl.ts b/packages/angular/cli/commands/extract-i18n-impl.ts similarity index 56% rename from packages/angular/cli/commands/xi18n-impl.ts rename to packages/angular/cli/commands/extract-i18n-impl.ts index ae0e9253a3ca..e41e9bcd26f1 100644 --- a/packages/angular/cli/commands/xi18n-impl.ts +++ b/packages/angular/cli/commands/extract-i18n-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -8,12 +8,12 @@ import { ArchitectCommand } from '../models/architect-command'; import { Arguments } from '../models/interface'; -import { Schema as Xi18nCommandSchema } from './xi18n'; +import { Schema as ExtractI18nCommandSchema } from './extract-i18n'; -export class Xi18nCommand extends ArchitectCommand { +export class ExtractI18nCommand extends ArchitectCommand { public readonly target = 'extract-i18n'; - public async run(options: Xi18nCommandSchema & Arguments) { + public async run(options: ExtractI18nCommandSchema & Arguments) { const version = process.version.substr(1).split('.'); if (Number(version[0]) === 12 && Number(version[1]) === 0) { this.logger.error( @@ -23,6 +23,11 @@ export class Xi18nCommand extends ArchitectCommand { return 1; } + const commandName = process.argv[2]; + if (['xi18n', 'i18n-extract'].includes(commandName)) { + this.logger.warn(`Warning: "ng ${commandName}" has been deprecated and will be removed in a future major version. Please use "ng extract-i18n" instead.`); + } + return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/xi18n.json b/packages/angular/cli/commands/extract-i18n.json similarity index 70% rename from packages/angular/cli/commands/xi18n.json rename to packages/angular/cli/commands/extract-i18n.json index c48d958440fa..85ecb0ffeeed 100644 --- a/packages/angular/cli/commands/xi18n.json +++ b/packages/angular/cli/commands/extract-i18n.json @@ -1,13 +1,13 @@ { "$schema": "http://json-schema.org/schema", - "$id": "ng-cli://commands/xi18n.json", + "$id": "ng-cli://commands/extract-i18n.json", "description": "Extracts i18n messages from source code.", "$longDescription": "", - "$aliases": ["i18n-extract"], + "$aliases": ["i18n-extract", "xi18n"], "$scope": "in", "$type": "architect", - "$impl": "./xi18n-impl#Xi18nCommand", + "$impl": "./extract-i18n-impl#ExtractI18nCommand", "type": "object", "allOf": [ diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index c32ccd7b87ea..6741be40f9e4 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts index 9c30c5e2f608..82edd6a441b4 100644 --- a/packages/angular/cli/commands/help-impl.ts +++ b/packages/angular/cli/commands/help-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/lint-impl.ts b/packages/angular/cli/commands/lint-impl.ts index 6edd8556f994..acf415fcdc2a 100644 --- a/packages/angular/cli/commands/lint-impl.ts +++ b/packages/angular/cli/commands/lint-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint-long.md index 03917ffb252e..480b069f1e2c 100644 --- a/packages/angular/cli/commands/lint-long.md +++ b/packages/angular/cli/commands/lint-long.md @@ -1,4 +1,7 @@ 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 +The default linting tool is [TSLint](https://palantir.github.io/tslint/), and the default configuration is specified in the project's `tslint.json` file. + +**Note**: TSLint has been discontinued and support has been deprecated in the Angular CLI. The options shown below are for the deprecated TSLint builder. +To opt-in using the community driven ESLint builder, see [angular-eslint](https://github.com/angular-eslint/angular-eslint#migrating-an-angular-cli-project-from-codelyzer-and-tslint) README. diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index b5f0221b3104..8bfd286e1b56 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -1,12 +1,13 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Arguments } from '../models/interface'; import { SchematicCommand } from '../models/schematic-command'; +import { ensureCompatibleNpm } from '../utilities/package-manager'; import { Schema as NewCommandSchema } from './new'; diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json index 89cfbda500dd..47dc5861726a 100644 --- a/packages/angular/cli/commands/new.json +++ b/packages/angular/cli/commands/new.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", "$id": "ng-cli://commands/new.json", - "description": "Creates a new workspace and an initial Angular app.", + "description": "Creates a new workspace and an initial Angular application.", "$longDescription": "./new.md", "$aliases": [ "n" ], @@ -16,13 +16,13 @@ "collection": { "type": "string", "aliases": [ "c" ], - "description": "A collection of schematics to use in generating the initial app." + "description": "A collection of schematics to use in generating the initial application." }, "verbose": { "type": "boolean", "default": false, "aliases": [ "v" ], - "description": "When true, adds more details to output logging." + "description": "Add more details to output logging." } }, "required": [] diff --git a/packages/angular/cli/commands/new.md b/packages/angular/cli/commands/new.md index 822deb745ee1..5d344b5d4312 100644 --- a/packages/angular/cli/commands/new.md +++ b/packages/angular/cli/commands/new.md @@ -1,16 +1,16 @@ -Creates and initializes a new Angular app that is the default project for a new workspace. +Creates and initializes a new Angular application that is the default project for a new workspace. Provides interactive prompts for optional configuration, such as adding routing support. All prompts can safely be allowed to default. * The new workspace folder is given the specified project name, and contains configuration files at the top level. -* By default, the files for a new initial 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. +* By default, the files for a new initial application (with the same name as the workspace) are placed in the `src/` subfolder. Corresponding end-to-end tests are placed in the `e2e/` subfolder. -* The new app's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name. +* The new application'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. +* Subsequent applications 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 +If you plan to have multiple applications in the workspace, you can create an empty workspace by setting the `--createApplication` option to false. +You can then use `ng generate application` to create an initial application. +This allows a workspace name different from the initial app name, and ensures that all applications reside in the `/projects` subfolder, matching the structure of the configuration file. \ 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 feefe731fa5b..3a0968e55898 100644 --- a/packages/angular/cli/commands/run-impl.ts +++ b/packages/angular/cli/commands/run-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json index 4111cc014a67..b6c3d64bb065 100644 --- a/packages/angular/cli/commands/run.json +++ b/packages/angular/cli/commands/run.json @@ -22,7 +22,7 @@ } }, "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.", + "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", "type": "string", "aliases": [ "c" ] } diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts index 04b51eee4e4a..fc5cf333b09c 100644 --- a/packages/angular/cli/commands/serve-impl.ts +++ b/packages/angular/cli/commands/serve-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts index 28f09df4d7b2..71d7b9147b36 100644 --- a/packages/angular/cli/commands/test-impl.ts +++ b/packages/angular/cli/commands/test-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts index b67d46aa1ec5..485fe6e05e65 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/commands/update-impl.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -11,13 +11,15 @@ import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as semver from 'semver'; +import { VERSION } from '../lib/cli'; import { PackageManager } from '../lib/config/schema'; import { Command } from '../models/command'; import { Arguments } from '../models/interface'; -import { runTempPackageBin } from '../tasks/install-package'; +import { SchematicEngineHost } from '../models/schematic-engine-host'; import { colors } from '../utilities/color'; +import { runTempPackageBin } from '../utilities/install-package'; import { writeErrorToLogFile } from '../utilities/log-file'; -import { getPackageManager } from '../utilities/package-manager'; +import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; import { PackageIdentifier, PackageManifest, @@ -41,11 +43,6 @@ const pickManifest = require('npm-pick-manifest') as ( const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json']; -const NG_VERSION_9_POST_MSG = colors.cyan( - '\nYour project has been updated to Angular version 9!\n' + - 'For more info, please see: https://v9.angular.io/guide/updating-to-version-9', -); - /** * Disable CLI version mismatch checks and forces usage of the invoked CLI * instead of invoking the local installed version. @@ -56,6 +53,8 @@ const disableVersionCheck = disableVersionCheckEnv !== '0' && disableVersionCheckEnv.toLowerCase() !== 'false'; +const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//; + export class UpdateCommand extends Command { public readonly allowMissingWorkspace = true; private workflow!: NodeWorkflow; @@ -63,16 +62,14 @@ export class UpdateCommand extends Command { async initialize() { this.packageManager = await getPackageManager(this.context.root); - this.workflow = new NodeWorkflow( - this.context.root, - { - packageManager: this.packageManager, - // __dirname -> favor @schematics/update from this package - // Otherwise, use packages from the active workspace (migrations) - resolvePaths: [__dirname, this.context.root], - schemaValidation: true, - }, - ); + this.workflow = new NodeWorkflow(this.context.root, { + packageManager: this.packageManager, + // __dirname -> favor @schematics/update from this package + // Otherwise, use packages from the active workspace (migrations) + resolvePaths: [__dirname, this.context.root], + schemaValidation: true, + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), + }); } private async executeSchematic( @@ -84,7 +81,7 @@ export class UpdateCommand extends Command { let logs: string[] = []; const files = new Set(); - const reporterSubscription = this.workflow.reporter.subscribe(event => { + const reporterSubscription = this.workflow.reporter.subscribe((event) => { // Strip leading slash to prevent confusion. const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path; @@ -114,11 +111,11 @@ export class UpdateCommand extends Command { } }); - const lifecycleSubscription = this.workflow.lifeCycle.subscribe(event => { + const lifecycleSubscription = this.workflow.lifeCycle.subscribe((event) => { if (event.kind == 'end' || event.kind == 'post-tasks-start') { if (!error) { // Output the logging queue, no error happened. - logs.forEach(log => this.logger.info(` ${log}`)); + logs.forEach((log) => this.logger.info(` ${log}`)); logs = []; } } @@ -141,12 +138,14 @@ export class UpdateCommand extends Command { return { success: !error, files }; } catch (e) { if (e instanceof UnsuccessfulWorkflowExecution) { - this.logger.error(`${colors.symbols.cross} Migration failed. See above for further details.\n`); + this.logger.error( + `${colors.symbols.cross} Migration failed. See above for further details.\n`, + ); } else { const logPath = writeErrorToLogFile(e); this.logger.fatal( `${colors.symbols.cross} Migration failed: ${e.message}\n` + - ` See "${logPath}" for further details.\n`, + ` See "${logPath}" for further details.\n`, ); } @@ -164,7 +163,7 @@ export class UpdateCommand extends Command { commit?: boolean, ): Promise { const collection = this.workflow.engine.createCollection(collectionPath); - const name = collection.listSchematicNames().find(name => name === migrationName); + const name = collection.listSchematicNames().find((name) => name === migrationName); if (!name) { this.logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`); @@ -213,15 +212,13 @@ export class UpdateCommand extends Command { return true; } - this.logger.info( - colors.cyan(`** Executing migrations of package '${packageName}' **\n`), - ); + this.logger.info(colors.cyan(`** Executing migrations of package '${packageName}' **\n`)); return this.executePackageMigrations(migrations, packageName, commit); } private async executePackageMigrations( - migrations: Iterable<{ name: string; description: string; collection: { name: string }}>, + migrations: Iterable<{ name: string; description: string; collection: { name: string } }>, packageName: string, commit = false, ): Promise { @@ -229,8 +226,9 @@ export class UpdateCommand extends Command { const [title, ...description] = migration.description.split('. '); this.logger.info( - colors.cyan(colors.symbols.pointer) + ' ' + - colors.bold(title.endsWith('.') ? title : title + '.'), + colors.cyan(colors.symbols.pointer) + + ' ' + + colors.bold(title.endsWith('.') ? title : title + '.'), ); if (description.length) { @@ -265,47 +263,42 @@ export class UpdateCommand extends Command { // tslint:disable-next-line:no-big-function async run(options: UpdateCommandSchema & Arguments) { - // Check if the @angular-devkit/schematics package can be resolved from the workspace root - // This works around issues with packages containing migrations that cannot directly depend on the package - // This check can be removed once the schematic runtime handles this situation - try { - require.resolve('@angular-devkit/schematics', { paths: [this.context.root] }); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - this.logger.fatal( - 'The "@angular-devkit/schematics" package cannot be resolved from the workspace root directory. ' + - 'This may be due to an unsupported node modules structure.\n' + - 'Please remove both the "node_modules" directory and the package lock file; and then reinstall.\n' + - 'If this does not correct the problem, ' + - 'please temporarily install the "@angular-devkit/schematics" package within the workspace. ' + - 'It can be removed once the update is complete.', + await ensureCompatibleNpm(this.context.root); + + // Check if the current installed CLI version is older than the latest compatible version. + if (!disableVersionCheck) { + const cliVersionToInstall = await this.checkCLIVersion( + options['--'], + options.verbose, + options.next, + ); + + if (cliVersionToInstall) { + this.logger.warn( + 'The installed Angular CLI version is outdated.\n' + + `Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`, ); - return 1; + return runTempPackageBin( + `@angular/cli@${cliVersionToInstall}`, + this.logger, + this.packageManager, + process.argv.slice(2), + ); } - - throw e; } - // Check if the current installed CLI version is older than the latest version. - if (!disableVersionCheck && await this.checkCLILatestVersion(options.verbose, options.next)) { - this.logger.warn( - `The installed local Angular CLI version is older than the latest ${options.next ? 'pre-release' : 'stable'} version.\n` + - 'Installing a temporary version to perform the update.', - ); - - return runTempPackageBin( - `@angular/cli@${options.next ? 'next' : 'latest'}`, - this.logger, - this.packageManager, - process.argv.slice(2), - ); - } + const logVerbose = (message: string) => { + if (options.verbose) { + this.logger.info(message); + } + }; if (options.all) { - const updateCmd = this.packageManager === PackageManager.Yarn - ? `'yarn upgrade-interactive' or 'yarn upgrade'` - : `'${this.packageManager} update'`; + const updateCmd = + this.packageManager === PackageManager.Yarn + ? `'yarn upgrade-interactive' or 'yarn upgrade'` + : `'${this.packageManager} update'`; this.logger.warn(` '--all' functionality has been removed as updating multiple packages at once is not recommended. @@ -328,7 +321,7 @@ export class UpdateCommand extends Command { return 1; } - if (packages.some(v => v.name === packageIdentifier.name)) { + if (packages.some((v) => v.name === packageIdentifier.name)) { this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`); return 1; @@ -409,7 +402,9 @@ export class UpdateCommand extends Command { if (options.migrateOnly) { if (!options.from && typeof options.migrateOnly !== 'string') { - this.logger.error('"from" option is required when using the "migrate-only" option without a migration name.'); + this.logger.error( + '"from" option is required when using the "migrate-only" option without a migration name.', + ); return 1; } else if (packages.length !== 1) { @@ -439,7 +434,7 @@ export class UpdateCommand extends Command { const packageJson = findPackageJson(this.context.root, packageName); if (packageJson) { packagePath = path.dirname(packageJson); - packageNode = await readPackageJson(packagePath); + packageNode = await readPackageJson(packageJson); } } @@ -472,8 +467,7 @@ export class UpdateCommand extends Command { if (migrations.startsWith('../')) { this.logger.error( - 'Package contains an invalid migrations field. ' + - 'Paths outside the package root are not permitted.', + 'Package contains an invalid migrations field. Paths outside the package root are not permitted.', ); return 1; @@ -499,9 +493,9 @@ export class UpdateCommand extends Command { } } - let success = false; + let result: boolean; if (typeof options.migrateOnly == 'string') { - success = await this.executeMigration( + result = await this.executeMigration( packageName, migrations, options.migrateOnly, @@ -518,8 +512,7 @@ export class UpdateCommand extends Command { const migrationRange = new semver.Range( '>' + from + ' <=' + (options.to || packageNode.version), ); - - success = await this.executeMigrations( + result = await this.executeMigrations( packageName, migrations, migrationRange, @@ -527,20 +520,7 @@ export class UpdateCommand extends Command { ); } - if (success) { - if ( - packageName === '@angular/core' - && options.from - && +options.from.split('.')[0] < 9 - && (options.to || packageNode.version).split('.')[0] === '9' - ) { - this.logger.info(NG_VERSION_9_POST_MSG); - } - - return 0; - } - - return 1; + return result ? 0 : 1; } const requests: { @@ -635,6 +615,35 @@ export class UpdateCommand extends Command { continue; } + if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) { + const { name, version } = node.package; + const toBeInstalledMajorVersion = +manifest.version.split('.')[0]; + const currentMajorVersion = +version.split('.')[0]; + + if (toBeInstalledMajorVersion - currentMajorVersion > 1) { + // Only allow updating a single version at a time. + if (currentMajorVersion < 6) { + // Before version 6, the major versions were not always sequential. + // Example @angular/core skipped version 3, @angular/cli skipped versions 2-5. + this.logger.error( + `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` + + `For more information about the update process, see https://update.angular.io/.`, + ); + } else { + const nextMajorVersionFromCurrent = currentMajorVersion + 1; + + this.logger.error( + `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` + + `Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` + + `to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` + + `For more information about the update process, see https://update.angular.io/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`, + ); + } + + return 1; + } + } + packagesToUpdate.push(requestIdentifier.toString()); } @@ -653,7 +662,8 @@ export class UpdateCommand extends Command { if (success && options.createCommits) { const committed = this.commit( - `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`); + `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`, + ); if (!committed) { return 1; } @@ -670,11 +680,70 @@ export class UpdateCommand extends Command { if (success && migrations) { for (const migration of migrations) { + // Resolve the package from the workspace root, as otherwise it will be resolved from the temp + // installed CLI version. + let packagePath; + logVerbose( + `Resolving migration package '${migration.package}' from '${this.context.root}'...`, + ); + try { + try { + packagePath = path.dirname( + // This may fail if the `package.json` is not exported as an entry point + require.resolve(path.join(migration.package, 'package.json'), { + paths: [this.context.root], + }), + ); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + // Fallback to trying to resolve the package's main entry point + packagePath = require.resolve(migration.package, { paths: [this.context.root] }); + } else { + throw e; + } + } + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + logVerbose(e.toString()); + this.logger.error( + `Migrations for package (${migration.package}) were not found.` + + ' The package could not be found in the workspace.', + ); + } else { + this.logger.error( + `Unable to resolve migrations for package (${migration.package}). [${e.message}]`, + ); + } + + return 1; + } + + let migrations; + + // Check if it is a package-local location + const localMigrations = path.join(packagePath, migration.collection); + if (fs.existsSync(localMigrations)) { + migrations = localMigrations; + } else { + // Try to resolve from package location. + // This avoids issues with package hoisting. + try { + migrations = require.resolve(migration.collection, { paths: [packagePath] }); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + this.logger.error(`Migrations for package (${migration.package}) were not found.`); + } else { + this.logger.error( + `Unable to resolve migrations for package (${migration.package}). [${e.message}]`, + ); + } + + return 1; + } + } const result = await this.executeMigrations( migration.package, - // Resolve the collection from the workspace root, as otherwise it will be resolved from the temp - // installed CLI version. - require.resolve(migration.collection, { paths: [this.context.root] }), + migrations, new semver.Range('>' + migration.from + ' <=' + migration.to), options.createCommits, ); @@ -683,10 +752,6 @@ export class UpdateCommand extends Command { return 0; } } - - if (migrations.some(m => m.package === '@angular/core' && m.to.split('.')[0] === '9' && +m.from.split('.')[0] < 9)) { - this.logger.info(NG_VERSION_9_POST_MSG); - } } return success ? 0 : 1; @@ -716,8 +781,7 @@ export class UpdateCommand extends Command { try { createCommit(message); } catch (err) { - this.logger.error( - `Failed to commit update (${message}):\n${err.stderr}`); + this.logger.error(`Failed to commit update (${message}):\n${err.stderr}`); return false; } @@ -726,8 +790,7 @@ export class UpdateCommand extends Command { const hash = findCurrentGitSha(); const shortMessage = message.split('\n')[0]; if (hash) { - this.logger.info(` Committed migration step (${getShortHash(hash)}): ${ - shortMessage}.`); + this.logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`); } else { // Commit was successful, but reading the hash was not. Something weird happened, // but nothing that would stop the update. Just log the weirdness and continue. @@ -740,7 +803,10 @@ export class UpdateCommand extends Command { private checkCleanGit(): boolean { try { - const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' }); + const topLevel = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + stdio: 'pipe', + }); const result = execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' }); if (result.trim().length === 0) { return true; @@ -763,14 +829,16 @@ export class UpdateCommand extends Command { } /** - * Checks if the current installed CLI version is older than the latest version. - * @returns `true` when the installed version is older. - */ - private async checkCLILatestVersion(verbose = false, next = false): Promise { - const { version: installedCLIVersion } = require('../package.json'); - - const LatestCLIManifest = await fetchPackageManifest( - `@angular/cli@${next ? 'next' : 'latest'}`, + * Checks if the current installed CLI version is older or newer than a compatible version. + * @returns the version to install or null when there is no update to install. + */ + private async checkCLIVersion( + packagesToUpdate: string[] | undefined, + verbose = false, + next = false, + ): Promise { + const { version } = await fetchPackageManifest( + `@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`, this.logger, { verbose, @@ -778,7 +846,38 @@ export class UpdateCommand extends Command { }, ); - return semver.lt(installedCLIVersion, LatestCLIManifest.version); + return VERSION.full === version ? null : version; + } + + private getCLIUpdateRunnerVersion( + packagesToUpdate: string[] | undefined, + next: boolean, + ): string | number { + if (next) { + return 'next'; + } + + const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r)); + if (updatingAngularPackage) { + // If we are updating any Angular package we can update the CLI to the target version because + // migrations for @angular/core@13 can be executed using Angular/cli@13. + // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`. + + // `@angular/cli@13` -> ['', 'angular/cli', '13'] + // `@angular/cli` -> ['', 'angular/cli'] + const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]); + + return semver.parse(tempVersion)?.major ?? 'latest'; + } + + // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in. + // Typically, we can assume that the `@angular/cli` was updated previously. + // Example: Angular official packages are typically updated prior to NGRX etc... + // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI. + + // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12. + // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic. + return VERSION.major; } } @@ -811,7 +910,7 @@ function createCommit(message: string) { */ function findCurrentGitSha(): string | null { try { - const hash = execSync('git rev-parse HEAD', {encoding: 'utf8', stdio: 'pipe'}); + const hash = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' }); return hash.trim(); } catch { diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update-long.md index 93be9c0f3c78..72df66ce35da 100644 --- a/packages/angular/cli/commands/update-long.md +++ b/packages/angular/cli/commands/update-long.md @@ -4,14 +4,19 @@ Perform a basic update to the current stable release of the core framework and C ng update @angular/cli @angular/core ``` -To update to the next beta or pre-release version, use the `--next=true` option. +To update to the next beta or pre-release version, use the `--next` option. To update from one major version to another, use the format -`ng update @angular/cli@^ @angular/core@^`. + +``` +ng update @angular/cli@^ @angular/core@^ +``` We recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release. -For example, use the following command to take the latest 9.x.x version and use that to update. +For example, use the following command to take the latest 10.x.x version and use that to update. -`ng update @angular/cli@^9 @angular/core@^9` +``` +ng update @angular/cli@^10 @angular/core@^10 +``` For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.io/). diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json index 0d1205c86078..b87e75a35fbb 100644 --- a/packages/angular/cli/commands/update.json +++ b/packages/angular/cli/commands/update.json @@ -44,7 +44,7 @@ "type": "boolean" }, "migrateOnly": { - "description": "Only perform a migration, does not update the installed version.", + "description": "Only perform a migration, do not update the installed version.", "oneOf": [ { "type": "boolean" diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts index 669ce2d2ba54..70a8fc6faa27 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version-impl.ts @@ -1,15 +1,14 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { JsonParseMode, isJsonObject, parseJson } from '@angular-devkit/core'; -import * as fs from 'fs'; import * as path from 'path'; import { Command } from '../models/command'; import { colors } from '../utilities/color'; +import { JSONFile } from '../utilities/json-file'; import { Schema as VersionCommandSchema } from './version'; interface PartialPackageInfo { @@ -169,15 +168,9 @@ export class VersionCommand extends Command { private getIvyWorkspace(): string { try { - const content = fs.readFileSync(path.resolve(this.context.root, 'tsconfig.json'), 'utf-8'); - const tsConfig = parseJson(content, JsonParseMode.Loose); - if (!isJsonObject(tsConfig)) { - return ''; - } - - const { angularCompilerOptions } = tsConfig; + const json = new JSONFile(path.resolve(this.context.root, 'tsconfig.json')); - return isJsonObject(angularCompilerOptions) && angularCompilerOptions.enableIvy === false + return json.get(['angularCompilerOptions', 'enableIvy']) === false ? 'No' : 'Yes'; } catch { diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index d7e3d913260a..d9878d04fa21 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index c8e163d12103..72ed61fb4ba3 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -64,6 +64,20 @@ "analytics": { "type": ["boolean", "string"], "description": "Share anonymous usage data with the Angular Team at Google." + }, + "analyticsSharing": { + "type": "object", + "properties": { + "tracking": { + "description": "Analytics sharing info tracking ID.", + "type": "string", + "pattern": "^GA-\\d+-\\d+$" + }, + "uuid": { + "description": "Analytics sharing info universally unique identifier.", + "type": "string" + } + } } }, "additionalProperties": false @@ -155,7 +169,7 @@ }, "skipTests": { "type": "boolean", - "description": "When true, does not create test files.", + "description": "Do not create test files.", "default": false } } @@ -197,7 +211,7 @@ }, "skipTests": { "type": "boolean", - "description": "When true, does not create test files.", + "description": "Do not create test files.", "default": false } } @@ -244,7 +258,7 @@ }, "skipTests": { "type": "boolean", - "description": "When true, does not create test files.", + "description": "Do not create test files.", "default": false } } @@ -259,7 +273,7 @@ }, "skipTests": { "type": "boolean", - "description": "When true, does not create test files.", + "description": "Do not create test files.", "default": false }, "skipImport": { @@ -285,7 +299,7 @@ "properties": { "skipTests": { "type": "boolean", - "description": "When true, does not create test files.", + "description": "Do not create test files.", "default": false } } @@ -478,7 +492,8 @@ "@angular-devkit/build-angular:karma", "@angular-devkit/build-angular:protractor", "@angular-devkit/build-angular:server", - "@angular-devkit/build-angular:tslint" + "@angular-devkit/build-angular:tslint", + "@angular-devkit/build-angular:ng-packagr" ] } }, @@ -584,6 +599,17 @@ "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/tslint" } } } + }, + { + "type": "object", + "properties": { + "builder": { "const": "@angular-devkit/build-angular:ng-packagr" }, + "options": { "$ref": "#/definitions/targetOptions/definitions/ngPackagr" }, + "configurations": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/ngPackagr" } + } + } } ] } @@ -619,11 +645,11 @@ "properties": { "browserTarget": { "type": "string", - "description": "Target to build." + "description": "A browser builder target to use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`." }, "serverTarget": { "type": "string", - "description": "Server target to use for rendering the app shell." + "description": "A server builder target to use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`." }, "appModuleBundle": { "type": "string", @@ -701,7 +727,7 @@ "additionalProperties": false }, "optimization": { - "description": "Enables optimization of the build output.", + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.", "oneOf": [ { "type": "object", @@ -712,12 +738,32 @@ "default": true }, "styles": { - "type": "boolean", "description": "Enables optimization of the styles output.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "minify": { + "type": "boolean", + "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.", + "default": true + }, + "inlineCritical": { + "type": "boolean", + "description": "Extract and inline critical CSS definitions to improve first paint time.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fonts": { - "description": "Enables optimization for fonts. This requires internet access.", + "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", "default": true, "oneOf": [ { @@ -725,7 +771,7 @@ "properties": { "inline": { "type": "boolean", - "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.", + "description": "Reduce render blocking requests by inlining external Google fonts and icons CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", "default": true } }, @@ -766,7 +812,7 @@ "default": false }, "sourceMap": { - "description": "Output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -774,22 +820,22 @@ "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "hidden": { "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", + "description": "Output source maps used for error reporting tools.", "default": false }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -802,12 +848,12 @@ }, "vendorChunk": { "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries.", + "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.", "default": true }, "commonChunk": { "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles.", + "description": "Generate a seperate bundle containing code used across multiple bundles.", "default": true }, "baseHref": { @@ -955,7 +1001,7 @@ "default": true }, "lazyModules": { - "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules with be discovered automatically.", + "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.", "type": "array", "items": { "type": "string" @@ -970,12 +1016,6 @@ }, "default": [] }, - "rebaseRootRelativeCssUrls": { - "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", - "type": "boolean", - "default": false, - "x-deprecated": true - }, "webWorkerTsConfig": { "type": "string", "description": "TypeScript configuration for Web Worker modules." @@ -1010,6 +1050,11 @@ { "type": "object", "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, "glob": { "type": "string", "description": "The pattern to match." @@ -1049,10 +1094,12 @@ "type": "object", "properties": { "src": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "replaceWith": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -1065,10 +1112,12 @@ "type": "object", "properties": { "replace": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "with": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -1171,7 +1220,7 @@ "properties": { "browserTarget": { "type": "string", - "description": "Target to serve." + "description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`." }, "port": { "type": "number", @@ -1212,13 +1261,13 @@ }, "open": { "type": "boolean", - "description": "When true, open the live-reload URL in default browser.", + "description": "Open the live-reload URL in default browser.", "default": false, "alias": "o" }, "liveReload": { "type": "boolean", - "description": "When true, reload the page on change using live-reload.", + "description": "Reload the page on change using live-reload.", "default": true }, "publicHost": { @@ -1239,31 +1288,31 @@ }, "disableHostCheck": { "type": "boolean", - "description": "When true, don't verify that connected clients are part of allowed hosts.", + "description": "Do not verify that connected clients are part of allowed hosts.", "default": false }, "hmr": { "type": "boolean", - "description": "When true, enable hot module replacement.", + "description": "Enable hot module replacement.", "default": false }, "watch": { "type": "boolean", - "description": "When true, rebuild on change.", + "description": "Rebuild on change.", "default": true }, "hmrWarning": { "type": "boolean", - "description": "When true, show a warning when the --hmr option is enabled.", + "description": "Show a warning when the --hmr option is enabled.", "default": true }, "servePathDefaultWarning": { "type": "boolean", - "description": "When true, show a warning when deploy-url/base-href use unsupported serve path values.", + "description": "Show a warning when deploy-url/base-href use unsupported serve path values.", "default": true }, "optimization": { - "description": "Enable optimization of the build output.", + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.", "default": false, "oneOf": [ { @@ -1271,12 +1320,12 @@ "properties": { "scripts": { "type": "boolean", - "description": "When true, enable optimization of the scripts output.", + "description": "Enable optimization of the scripts output.", "default": true }, "styles": { "type": "boolean", - "description": "When true, enable optimization of the styles output.", + "description": "Enable optimization of the styles output.", "default": true } }, @@ -1292,7 +1341,7 @@ "description": "Build using ahead-of-time compilation." }, "sourceMap": { - "description": "When true, output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -1300,17 +1349,17 @@ "properties": { "scripts": { "type": "boolean", - "description": "When true, output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "When true, output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "vendor": { "type": "boolean", - "description": "When true, resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -1323,11 +1372,11 @@ }, "vendorChunk": { "type": "boolean", - "description": "When true, use a separate bundle containing only vendor libraries." + "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development." }, "commonChunk": { "type": "boolean", - "description": "When true, use a separate bundle containing code used across multiple bundles." + "description": "Generate a seperate bundle containing code used across multiple bundles." }, "baseHref": { "type": "string", @@ -1339,11 +1388,11 @@ }, "verbose": { "type": "boolean", - "description": "When true, add more details to output logging." + "description": "Add more details to output logging." }, "progress": { "type": "boolean", - "description": "When true, log progress to the console while building." + "description": "Log progress to the console while building." } }, "additionalProperties": false @@ -1354,7 +1403,7 @@ "properties": { "browserTarget": { "type": "string", - "description": "Target to extract from." + "description": "A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`." }, "format": { "type": "string", @@ -1366,7 +1415,9 @@ "xlif", "xliff", "xlf2", - "xliff2" + "xliff2", + "json", + "arb" ] }, "i18nFormat": { @@ -1380,7 +1431,9 @@ "xlif", "xliff", "xlf2", - "xliff2" + "xliff2", + "json", + "arb" ] }, "i18nLocale": { @@ -1468,7 +1521,7 @@ "additionalProperties": false }, "sourceMap": { - "description": "Output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -1476,17 +1529,17 @@ "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -1592,6 +1645,11 @@ { "type": "object", "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, "glob": { "type": "string", "description": "The pattern to match." @@ -1667,7 +1725,7 @@ }, "devServerTarget": { "type": "string", - "description": "Dev server target to run tests against." + "description": "A dev-server builder target to run tests against in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`." }, "grep": { "type": "string", @@ -1745,7 +1803,7 @@ "additionalProperties": false }, "optimization": { - "description": "Enables optimization of the build output.", + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking and dead-code elimination. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.", "default": false, "oneOf": [ { @@ -1786,7 +1844,7 @@ "description": "The path where style resources will be placed, relative to outputPath." }, "sourceMap": { - "description": "Output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -1794,22 +1852,22 @@ "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "hidden": { "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", + "description": "Output source maps used for error reporting tools.", "default": false }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -1937,10 +1995,12 @@ "type": "object", "properties": { "src": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "replaceWith": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -1953,10 +2013,12 @@ "type": "object", "properties": { "replace": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "with": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -2051,6 +2113,26 @@ } }, "additionalProperties": false + }, + "ngPackagr": { + "description": "ng-packagr target options for Build Architect. Use to build library projects.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The file path for the ng-packagr configuration file, relative to the current workspace." + }, + "tsConfig": { + "type": "string", + "description": "The full path for the TypeScript configuration file, relative to the current workspace." + }, + "watch": { + "type": "boolean", + "description": "Run build when files change.", + "default": false + } + }, + "additionalProperties": false } } }, @@ -2062,6 +2144,7 @@ "default": "warning" }, "localize": { + "description": "Translate the bundles in one or more locales.", "oneOf": [ { "type": "boolean", diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 2740fa69bf43..7a67dabea1c8 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -81,33 +81,41 @@ if (process.env['NG_CLI_PROFILING']) { const projectLocalCli = require.resolve('@angular/cli', { paths: [process.cwd()] }); cli = await import(projectLocalCli); - // This was run from a global, check local version. - if (await isWarningEnabled('versionMismatch')) { - const globalVersion = new SemVer(require('../package.json').version); - - // Older versions might not have the VERSION export - let localVersion = cli.VERSION?.full; - if (!localVersion) { - try { - localVersion = require(path.join(path.dirname(projectLocalCli), '../../package.json')) - .version; - } catch (error) { - // tslint:disable-next-line no-console - console.error( - 'Version mismatch check skipped. Unable to retrieve local version: ' + error, - ); - } - } + const globalVersion = new SemVer(require('../package.json').version); - let shouldWarn = false; + // Older versions might not have the VERSION export + let localVersion = cli.VERSION?.full; + if (!localVersion) { try { - shouldWarn = !!localVersion && globalVersion.compare(localVersion) > 0; + localVersion = require(path.join(path.dirname(projectLocalCli), '../../package.json')) + .version; } catch (error) { // tslint:disable-next-line no-console - console.error('Version mismatch check skipped. Unable to compare local version: ' + error); + console.error( + 'Version mismatch check skipped. Unable to retrieve local version: ' + error, + ); } + } + + let isGlobalGreater = false; + try { + isGlobalGreater = !!localVersion && globalVersion.compare(localVersion) > 0; + } catch (error) { + // tslint:disable-next-line no-console + console.error('Version mismatch check skipped. Unable to compare local version: ' + error); + } - if (shouldWarn) { + if (isGlobalGreater) { + // If using the update command and the global version is greater, use the newer update command + // This allows improvements in update to be used in older versions that do not have bootstrapping + if ( + process.argv[2] === 'update' && + cli.VERSION && + cli.VERSION.major - globalVersion.major <= 1 + ) { + cli = await import('./cli'); + } else if (await isWarningEnabled('versionMismatch')) { + // Otherwise, use local version and warn if global is newer than local const warning = `Your global Angular CLI version (${globalVersion}) is greater than your local ` + `version (${localVersion}). The local Angular CLI version is used.\n\n` + diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts index 1118c2311b8d..76474dfc60a6 100644 --- a/packages/angular/cli/models/analytics.ts +++ b/packages/angular/cli/models/analytics.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -8,7 +8,6 @@ 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'; @@ -357,21 +356,21 @@ export function setAnalyticsConfig(level: 'global' | 'local', value: string | bo throw new Error(`Could not find ${level} workspace.`); } - const configValue = config.value; - const cli: json.JsonValue = configValue['cli'] || (configValue['cli'] = {}); + const cli = config.get(['cli']); - if (!json.isJsonObject(cli)) { + if (cli !== undefined && !json.isJsonObject(cli as json.JsonValue)) { throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`); } if (value === true) { value = uuidV4(); } - cli['analytics'] = value; - const output = JSON.stringify(configValue, null, 2); - writeFileSync(configPath, output); + config.modify(['cli', 'analytics'], value); + config.save(); + analyticsDebug('done'); + } /** @@ -389,7 +388,7 @@ export async function promptGlobalAnalytics(force = false) { 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. + how to change this setting, see https://angular.io/analytics. `, default: false, }, @@ -447,7 +446,7 @@ export async function promptProjectAnalytics(force = false): Promise { 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. + details and how to change this setting, see https://angular.io/analytics. `, default: false, diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index c9a9573dd711..57f8a9a88dd9 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index b8ec88e57883..3fecd24d7639 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -1,12 +1,11 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { - JsonParseMode, analytics, isJsonObject, json, @@ -18,6 +17,7 @@ import { import { readFileSync } from 'fs'; import { join, resolve } from 'path'; import { AngularWorkspace } from '../utilities/config'; +import { readAndParseJson } from '../utilities/json-file'; import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; import { getGlobalAnalytics, @@ -39,6 +39,7 @@ const standardCommands = { 'config': '../commands/config.json', 'doc': '../commands/doc.json', 'e2e': '../commands/e2e.json', + 'extract-i18n': '../commands/extract-i18n.json', 'make-this-awesome': '../commands/easter-egg.json', 'generate': '../commands/generate.json', 'help': '../commands/help.json', @@ -49,7 +50,6 @@ const standardCommands = { 'test': '../commands/test.json', 'update': '../commands/update.json', 'version': '../commands/version.json', - 'xi18n': '../commands/xi18n.json', }; export interface CommandMapOptions { @@ -97,8 +97,7 @@ async function loadCommandDescription( 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 }); + const schema = readAndParseJson(schemaPath); if (!isJsonObject(schema)) { throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); } diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 4f7eac9fb837..eec0c3eec802 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -54,7 +54,10 @@ export abstract class Command } async printJsonHelp(_options: T & Arguments): Promise { - this.logger.info(JSON.stringify(this.description)); + const replacer = (key: string, value: string) => key === 'name' + ? strings.dasherize(value) + : value; + this.logger.info(JSON.stringify(this.description, replacer, 2)); return 0; } @@ -160,7 +163,7 @@ export abstract class Command this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics }); } - abstract async run(options: T & Arguments): Promise; + abstract run(options: T & Arguments): Promise; async validateAndRun(options: T & Arguments): Promise { if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { diff --git a/packages/angular/cli/models/error.ts b/packages/angular/cli/models/error.ts index 5d9d323ed103..dacdd2f3a38d 100644 --- a/packages/angular/cli/models/error.ts +++ b/packages/angular/cli/models/error.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 338b6310bce5..f5b3ee4de60e 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -153,6 +153,12 @@ export interface Option { * If this is falsey, do not report this option. */ userAnalytics?: number; + + /** + * Deprecation. If this flag is not false a warning will be shown on the console. Either `true` + * or a string to show the user as a notice. + */ + deprecated?: boolean | string; } /** diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts index e09405132a33..951e35b1dadd 100644 --- a/packages/angular/cli/models/parser.ts +++ b/packages/angular/cli/models/parser.ts @@ -1,10 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license - * */ import { BaseException, logging, strings } from '@angular-devkit/core'; import { Arguments, Option, OptionType, Value } from './interface'; @@ -211,6 +210,13 @@ function _assignOption( errors.push(error); ignored.push(arg); } + + if (/^[a-z]+[A-Z]/.test(key)) { + warnings.push( + 'Support for camel case arguments has been deprecated and will be removed in a future major version.\n' + + `Use '--${strings.dasherize(key)}' instead of '--${key}'.`, + ); + } } return consumedNextArg; diff --git a/packages/angular/cli/models/parser_spec.ts b/packages/angular/cli/models/parser_spec.ts index cd6b3c09e801..152cdb36fe58 100644 --- a/packages/angular/cli/models/parser_spec.ts +++ b/packages/angular/cli/models/parser_spec.ts @@ -1,10 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license - * */ // tslint:disable:no-global-tslint-disable no-big-function import { logging } from '@angular-devkit/core'; diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 92f87ad0075b..f67024eb7761 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -1,12 +1,11 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { - json, logging, normalize, schema, @@ -24,25 +23,20 @@ import { FileSystemCollection, FileSystemEngine, FileSystemSchematic, - FileSystemSchematicDescription, NodeWorkflow, } from '@angular-devkit/schematics/tools'; import * as inquirer from 'inquirer'; import * as systemPath from 'path'; import { colors } from '../utilities/color'; -import { - getProjectByCwd, - getSchematicDefaults, - getWorkspace, - getWorkspaceRaw, -} from '../utilities/config'; +import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../utilities/config'; import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { getPackageManager } from '../utilities/package-manager'; +import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; import { isTTY } from '../utilities/tty'; import { isPackageNameSafeForAnalytics } from './analytics'; import { BaseCommandOptions, Command } from './command'; import { Arguments, CommandContext, CommandDescription, Option } from './interface'; import { parseArguments, parseFreeFormArguments } from './parser'; +import { SchematicEngineHost } from './schematic-engine-host'; export interface BaseSchematicSchema { debug?: boolean; @@ -257,6 +251,14 @@ export abstract class SchematicCommand< // Global : [__dirname, process.cwd()], schemaValidation: true, + optionTransforms: [ + // Add configuration file defaults + async (schematic, current) => ({ + ...(await getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName())), + ...current, + }), + ], + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), }); const getProjectName = () => { @@ -284,16 +286,6 @@ export abstract class SchematicCommand< return undefined; }; - const defaultOptionTransform = async ( - schematic: FileSystemSchematicDescription, - current: {}, - ) => ({ - ...(await getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName())), - ...current, - }); - - workflow.engineHost.registerOptionsTransform(defaultOptionTransform); - workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); workflow.registry.addSmartDefaultProvider('projectName', getProjectName); workflow.registry.useXDeprecatedProvider(msg => this.logger.warn(msg)); @@ -322,6 +314,32 @@ export abstract class SchematicCommand< const validator = definition.validator; if (validator) { question.validate = input => validator(input); + + // Filter allows transformation of the value prior to validation + question.filter = async (input) => { + for (const type of definition.propertyTypes) { + let value; + switch (type) { + case 'string': + value = String(input); + break; + case 'integer': + case 'number': + value = Number(input); + break; + default: + value = input; + break; + } + // Can be a string if validation fails + const isValid = (await validator(value)) === true; + if (isValid) { + return value; + } + } + + return input; + }; } switch (definition.type) { @@ -407,43 +425,6 @@ export abstract class SchematicCommand< collectionName = schematic.collection.description.name; schematicName = schematic.description.name; - // TODO: Remove warning check when 'targets' is default - if (collectionName !== this.defaultCollectionName) { - const [ast, configPath] = getWorkspaceRaw('local'); - if (ast) { - const projectsKeyValue = ast.properties.find(p => p.key.value === 'projects'); - if (!projectsKeyValue || projectsKeyValue.value.kind !== 'object') { - return; - } - - const positions: json.Position[] = []; - for (const projectKeyValue of projectsKeyValue.value.properties) { - const projectNode = projectKeyValue.value; - if (projectNode.kind !== 'object') { - continue; - } - const targetsKeyValue = projectNode.properties.find(p => p.key.value === 'targets'); - if (targetsKeyValue) { - positions.push(targetsKeyValue.start); - } - } - - if (positions.length > 0) { - const warning = tags.oneLine` - Warning: This command may not execute successfully. - The package/collection may not support the 'targets' field within '${configPath}'. - This can be corrected by renaming the following 'targets' fields to 'architect': - `; - - const locations = positions - .map((p, i) => `${i + 1}) Line: ${p.line + 1}; Column: ${p.character + 1}`) - .join('\n'); - - this.logger.warn(warning + '\n' + locations + '\n'); - } - } - } - // Set the options of format "path". let o: Option[] | null = null; let args: Arguments; @@ -522,6 +503,16 @@ export abstract class SchematicCommand< } }); + // Temporary compatibility check for NPM 7 + if (collectionName === '@schematics/angular' && schematicName === 'ng-new') { + if ( + !input.skipInstall && + (input.packageManager === undefined || input.packageManager === 'npm') + ) { + await ensureCompatibleNpm(this.context.root); + } + } + return new Promise(resolve => { workflow .execute({ @@ -539,7 +530,7 @@ export abstract class SchematicCommand< // "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}`); + this.logger.fatal(`An error occurred:\n${err.message}\n${err.stack}`); } else { this.logger.fatal(err.message); } diff --git a/packages/angular/cli/models/schematic-engine-host.ts b/packages/angular/cli/models/schematic-engine-host.ts new file mode 100644 index 000000000000..df3482e5adf7 --- /dev/null +++ b/packages/angular/cli/models/schematic-engine-host.ts @@ -0,0 +1,189 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { RuleFactory, SchematicsException, Tree } from '@angular-devkit/schematics'; +import { NodeModulesEngineHost } from '@angular-devkit/schematics/tools'; +import { readFileSync } from 'fs'; +import { parse as parseJson } from 'jsonc-parser'; +import { dirname, resolve } from 'path'; +import { Script } from 'vm'; + +/** + * Environment variable to control schematic package redirection + * Default: Angular schematics only + */ +const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase(); + +function shouldWrapSchematic(schematicFile: string): boolean { + // Check environment variable if present + if (schematicRedirectVariable !== undefined) { + switch (schematicRedirectVariable) { + case '0': + case 'false': + case 'off': + case 'none': + return false; + case 'all': + return true; + } + } + + // Never wrap `@schematics/update` when executed directly + // It communicates with the update command via `global` + if (/[\/\\]node_modules[\/\\]@schematics[\/\\]update[\/\\]/.test(schematicFile)) { + return false; + } + + // Default is only first-party Angular schematic packages + // Angular schematics are safe to use in the wrapped VM context + return /[\/\\]node_modules[\/\\]@(?:angular|schematics|nguniversal)[\/\\]/.test(schematicFile); +} + +export class SchematicEngineHost extends NodeModulesEngineHost { + protected _resolveReferenceString(refString: string, parentPath: string) { + const [path, name] = refString.split('#', 2); + // Mimic behavior of ExportStringRef class used in default behavior + const fullPath = path[0] === '.' ? resolve(parentPath ?? process.cwd(), path) : path; + + const schematicFile = require.resolve(fullPath, { paths: [parentPath] }); + + if (shouldWrapSchematic(schematicFile)) { + const schematicPath = dirname(schematicFile); + + const moduleCache = new Map(); + const factoryInitializer = wrap( + schematicFile, + schematicPath, + moduleCache, + name || 'default', + ) as () => RuleFactory<{}>; + + const factory = factoryInitializer(); + if (!factory || typeof factory !== 'function') { + return null; + } + + return { ref: factory, path: schematicPath }; + } + + // All other schematics use default behavior + return super._resolveReferenceString(refString, parentPath); + } +} + +/** + * Minimal shim modules for legacy deep imports of `@schematics/angular` + */ +const legacyModules: Record = { + '@schematics/angular/utility/config': { + getWorkspace(host: Tree) { + const path = '/.angular.json'; + const data = host.read(path); + if (!data) { + throw new SchematicsException(`Could not find (${path})`); + } + + return parseJson(data.toString(), [], { allowTrailingComma: true }); + }, + }, + '@schematics/angular/utility/project': { + buildDefaultPath(project: { sourceRoot?: string; root: string; projectType: string }): string { + const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; + + return `${root}${project.projectType === 'application' ? 'app' : 'lib'}`; + }, + }, +}; + +/** + * Wrap a JavaScript file in a VM context to allow specific Angular dependencies to be redirected. + * This VM setup is ONLY intended to redirect dependencies. + * + * @param schematicFile A JavaScript schematic file path that should be wrapped. + * @param schematicDirectory A directory that will be used as the location of the JavaScript file. + * @param moduleCache A map to use for caching repeat module usage and proper `instanceof` support. + * @param exportName An optional name of a specific export to return. Otherwise, return all exports. + */ +function wrap( + schematicFile: string, + schematicDirectory: string, + moduleCache: Map, + exportName?: string, +): () => unknown { + const { createRequire, createRequireFromPath } = require('module'); + // Node.js 10.x does not support `createRequire` so fallback to `createRequireFromPath` + // `createRequireFromPath` is deprecated in 12+ and can be removed once 10.x support is removed + const scopedRequire = createRequire?.(schematicFile) || createRequireFromPath(schematicFile); + + const customRequire = function (id: string) { + if (legacyModules[id]) { + // Provide compatibility modules for older versions of @angular/cdk + return legacyModules[id]; + } else if (id.startsWith('@angular-devkit/') || id.startsWith('@schematics/')) { + // Resolve from inside the `@angular/cli` project + const packagePath = require.resolve(id); + + return require(packagePath); + } else if (id.startsWith('.') || id.startsWith('@angular/cdk')) { + // Wrap relative files inside the schematic collection + // Also wrap `@angular/cdk`, it contains helper utilities that import core schematic packages + + // Resolve from the original file + const modulePath = scopedRequire.resolve(id); + + // Use cached module if available + const cachedModule = moduleCache.get(modulePath); + if (cachedModule) { + return cachedModule; + } + + // Do not wrap vendored third-party packages or JSON files + if ( + !/[\/\\]node_modules[\/\\]@schematics[\/\\]angular[\/\\]third_party[\/\\]/.test( + modulePath, + ) && + !modulePath.endsWith('.json') + ) { + // Wrap module and save in cache + const wrappedModule = wrap(modulePath, dirname(modulePath), moduleCache)(); + moduleCache.set(modulePath, wrappedModule); + + return wrappedModule; + } + } + + // All others are required directly from the original file + return scopedRequire(id); + }; + + // Setup a wrapper function to capture the module's exports + const schematicCode = readFileSync(schematicFile, 'utf8'); + // `module` is required due to @angular/localize ng-add being in UMD format + const headerCode = '(function() {\nvar exports = {};\nvar module = { exports };\n'; + const footerCode = exportName ? `\nreturn exports['${exportName}'];});` : '\nreturn exports;});'; + + const script = new Script(headerCode + schematicCode + footerCode, { + filename: schematicFile, + lineOffset: 3, + }); + + const context = { + __dirname: schematicDirectory, + __filename: schematicFile, + Buffer, + console, + process, + get global() { + return this; + }, + require: customRequire, + }; + + const exportsFactory = script.runInNewContext(context); + + return exportsFactory; +} diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 35b97a5fc985..4953c40e409d 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -32,26 +32,28 @@ "@schematics/update": "0.0.0", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.1", - "debug": "4.2.0", - "ini": "1.3.5", + "debug": "4.3.1", + "ini": "2.0.0", "inquirer": "7.3.3", + "jsonc-parser": "3.0.0", "npm-package-arg": "8.1.0", "npm-pick-manifest": "6.1.0", - "open": "7.3.0", - "pacote": "9.5.12", - "resolve": "1.17.0", + "open": "7.4.0", + "ora": "5.3.0", + "pacote": "11.2.4", + "resolve": "1.19.0", "rimraf": "3.0.2", - "semver": "7.3.2", - "symbol-observable": "2.0.3", + "semver": "7.3.4", + "symbol-observable": "3.0.0", "universal-analytics": "0.4.23", - "uuid": "8.3.1" + "uuid": "8.3.2" }, "ng-update": { "migrations": "@schematics/angular/migrations/migration-collection.json", "packageGroup": { "@angular/cli": "0.0.0", + "@angular-devkit/architect": "0.0.0", "@angular-devkit/build-angular": "0.0.0", - "@angular-devkit/build-ng-packagr": "0.0.0", "@angular-devkit/build-webpack": "0.0.0", "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0" diff --git a/packages/angular/cli/utilities/color.ts b/packages/angular/cli/utilities/color.ts index 6e7d11cb7fe2..c729fa6a5811 100644 --- a/packages/angular/cli/utilities/color.ts +++ b/packages/angular/cli/utilities/color.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts index 029aaa156858..9bbef68eeee6 100644 --- a/packages/angular/cli/utilities/config.ts +++ b/packages/angular/cli/utilities/config.ts @@ -1,30 +1,46 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { - JsonAstObject, - JsonObject, - JsonParseMode, - json, - parseJson, - parseJsonAst, - workspaces, -} from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { json, workspaces } from '@angular-devkit/core'; +import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; import * as os from 'os'; import * as path from 'path'; import { findUp } from './find-up'; +import { JSONFile, readAndParseJson } from './json-file'; function isJsonObject(value: json.JsonValue | undefined): value is json.JsonObject { return value !== undefined && json.isJsonObject(value); } +function createWorkspaceHost(): workspaces.WorkspaceHost { + return { + async readFile(path) { + return readFileSync(path, 'utf-8'); + }, + async writeFile(path, data) { + writeFileSync(path, data); + }, + async isDirectory(path) { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } + }, + async isFile(path) { + try { + return statSync(path).isFile(); + } catch { + return false; + } + }, + }; +} + function getSchemaLocation(): string { return path.join(__dirname, '../lib/config/schema.json'); } @@ -116,7 +132,7 @@ export class AngularWorkspace { const result = await workspaces.readWorkspace( workspaceFilePath, - workspaces.createWorkspaceHost(new NodeJsSyncHost()), + createWorkspaceHost(), workspaces.WorkspaceFormat.JSON, ); @@ -143,13 +159,7 @@ export async function getWorkspace( } try { - const result = await workspaces.readWorkspace( - configPath, - workspaces.createWorkspaceHost(new NodeJsSyncHost()), - workspaces.WorkspaceFormat.JSON, - ); - - const workspace = new AngularWorkspace(result.workspace, configPath); + const workspace = await AngularWorkspace.load(configPath); cachedWorkspaces.set(level, workspace); return workspace; @@ -175,7 +185,7 @@ export function createGlobalSettings(): string { export function getWorkspaceRaw( level: 'local' | 'global' = 'local', -): [JsonAstObject | null, string | null] { +): [JSONFile | null, string | null] { let configPath = level === 'local' ? projectFilePath() : globalFilePath(); if (!configPath) { @@ -186,28 +196,11 @@ export function getWorkspaceRaw( } } - const data = readFileSync(configPath); - let start = 0; - if (data.length > 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) { - // Remove BOM - start = 3; - } - const content = data.toString('utf-8', start); - const ast = parseJsonAst(content, JsonParseMode.Loose); - - if (ast.kind != 'object') { - throw new Error(`Invalid JSON file: ${configPath}`); - } - - return [ast, configPath]; + return [new JSONFile(configPath), configPath]; } -export async function validateWorkspace(data: JsonObject): Promise { - const schemaContent = readFileSync( - path.join(__dirname, '..', 'lib', 'config', 'schema.json'), - 'utf-8', - ); - const schema = parseJson(schemaContent, JsonParseMode.Loose) as json.schema.JsonSchema; +export async function validateWorkspace(data: json.JsonObject): Promise { + const schema = readAndParseJson(path.join(__dirname, '../lib/config/schema.json')) as json.schema.JsonSchema; const { formats } = await import('@angular-devkit/schematics'); const registry = new json.schema.CoreSchemaRegistry(formats.standardFormats); const validator = await registry.compile(schema).toPromise(); @@ -321,13 +314,12 @@ export function migrateLegacyGlobalConfig(): boolean { if (homeDir) { const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json'); if (existsSync(legacyGlobalConfigPath)) { - const content = readFileSync(legacyGlobalConfigPath, 'utf-8'); - const legacy = parseJson(content, JsonParseMode.Loose); + const legacy = readAndParseJson(legacyGlobalConfigPath); if (!isJsonObject(legacy)) { return false; } - const cli: JsonObject = {}; + const cli: json.JsonObject = {}; if ( legacy.packageManager && @@ -346,7 +338,7 @@ export function migrateLegacyGlobalConfig(): boolean { } if (isJsonObject(legacy.warnings)) { - const warnings: JsonObject = {}; + const warnings: json.JsonObject = {}; if (typeof legacy.warnings.versionMismatch == 'boolean') { warnings['versionMismatch'] = legacy.warnings.versionMismatch; } @@ -374,9 +366,7 @@ function getLegacyPackageManager(): string | null { if (homeDir) { const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json'); if (existsSync(legacyGlobalConfigPath)) { - const content = readFileSync(legacyGlobalConfigPath, 'utf-8'); - - const legacy = parseJson(content, JsonParseMode.Loose); + const legacy = readAndParseJson(legacyGlobalConfigPath); if (!isJsonObject(legacy)) { return null; } diff --git a/packages/angular/cli/utilities/find-up.ts b/packages/angular/cli/utilities/find-up.ts index 81891a96e565..3427d7ba15f4 100644 --- a/packages/angular/cli/utilities/find-up.ts +++ b/packages/angular/cli/utilities/find-up.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/tasks/install-package.ts b/packages/angular/cli/utilities/install-package.ts similarity index 95% rename from packages/angular/cli/tasks/install-package.ts rename to packages/angular/cli/utilities/install-package.ts index a4862a3966b3..8bdf179230e3 100644 --- a/packages/angular/cli/tasks/install-package.ts +++ b/packages/angular/cli/utilities/install-package.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -26,7 +26,7 @@ interface PackageManagerOptions { export function installPackage( packageName: string, - logger: logging.Logger, + logger: logging.Logger | undefined, packageManager: PackageManager = PackageManager.Npm, save: Exclude = true, extraArgs: string[] = [], @@ -40,7 +40,7 @@ export function installPackage( packageManagerArgs.silent, ]; - logger.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); + logger?.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); if (save === 'devDependencies') { installArgs.push(packageManagerArgs.saveDev); @@ -61,12 +61,12 @@ export function installPackage( throw new Error(errorMessage + `Package install failed${errorMessage ? ', see above' : ''}.`); } - logger.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); + logger?.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); } export function installTempPackage( packageName: string, - logger: logging.Logger, + logger: logging.Logger | undefined, packageManager: PackageManager = PackageManager.Npm, extraArgs?: string[], ): string { diff --git a/packages/angular/cli/utilities/json-file.ts b/packages/angular/cli/utilities/json-file.ts new file mode 100644 index 000000000000..7163df2b0751 --- /dev/null +++ b/packages/angular/cli/utilities/json-file.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { JsonValue } from '@angular-devkit/core'; +import { readFileSync, writeFileSync } from 'fs'; +import { + Node, ParseError, applyEdits, findNodeAtLocation, + getNodeValue, modify, parse, parseTree, printParseErrorCode, +} from 'jsonc-parser'; + +export type InsertionIndex = (properties: string[]) => number; +export type JSONPath = (string | number)[]; + +/** @internal */ +export class JSONFile { + content: string; + + constructor( + private readonly path: string, + ) { + const buffer = readFileSync(this.path); + if (buffer) { + this.content = buffer.toString(); + } else { + throw new Error(`Could not read '${path}'.`); + } + } + + private _jsonAst: Node | undefined; + private get JsonAst(): Node | undefined { + if (this._jsonAst) { + return this._jsonAst; + } + + const errors: ParseError[] = []; + this._jsonAst = parseTree(this.content, errors, { allowTrailingComma: true }); + if (errors.length) { + formatError(this.path, errors); + } + + return this._jsonAst; + } + + get(jsonPath: JSONPath): unknown { + const jsonAstNode = this.JsonAst; + if (!jsonAstNode) { + return undefined; + } + + if (jsonPath.length === 0) { + return getNodeValue(jsonAstNode); + } + + const node = findNodeAtLocation(jsonAstNode, jsonPath); + + return node === undefined ? undefined : getNodeValue(node); + } + + modify(jsonPath: JSONPath, value: JsonValue | undefined, insertInOrder?: InsertionIndex | false): boolean { + if (value === undefined && this.get(jsonPath) === undefined) { + // Cannot remove a value which doesn't exist. + return false; + } + + let getInsertionIndex: InsertionIndex | undefined; + if (insertInOrder === undefined) { + const property = jsonPath.slice(-1)[0]; + getInsertionIndex = properties => [...properties, property].sort().findIndex(p => p === property); + } else if (insertInOrder !== false) { + getInsertionIndex = insertInOrder; + } + + const edits = modify( + this.content, + jsonPath, + value, + { + getInsertionIndex, + formattingOptions: { + insertSpaces: true, + tabSize: 2, + }, + }, + ); + + if (edits.length === 0) { + return false; + } + + this.content = applyEdits(this.content, edits); + this._jsonAst = undefined; + + return true; + } + + save(): void { + writeFileSync(this.path, this.content); + } +} + +// tslint:disable-next-line: no-any +export function readAndParseJson(path: string): any { + const errors: ParseError[] = []; + const content = parse(readFileSync(path, 'utf-8'), errors, { allowTrailingComma: true }); + if (errors.length) { + formatError(path, errors); + } + + return content; +} + +function formatError(path: string, errors: ParseError[]): never { + const { error, offset } = errors[0]; + throw new Error(`Failed to parse "${path}" as JSON AST Object. ${printParseErrorCode(error)} at location: ${offset}.`); +} + +// tslint:disable-next-line: no-any +export function parseJson(content: string): any { + return parse(content, undefined, { allowTrailingComma: true }); +} diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts index abd856c4196a..d50fd7fbabd4 100644 --- a/packages/angular/cli/utilities/json-schema.ts +++ b/packages/angular/cli/utilities/json-schema.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -251,6 +251,11 @@ export async function parseJsonSchemaToOptions( const xUserAnalytics = current['x-user-analytics']; const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined; + // Deprecated is set only if it's true or a string. + const xDeprecated = current['x-deprecated']; + const deprecated = (xDeprecated === true || typeof xDeprecated === 'string') + ? xDeprecated : undefined; + const option: Option = { name, description: '' + (current.description === undefined ? '' : current.description), @@ -262,6 +267,7 @@ export async function parseJsonSchemaToOptions( ...format !== undefined ? { format } : {}, hidden, ...userAnalytics ? { userAnalytics } : {}, + ...deprecated !== undefined ? { deprecated } : {}, ...positional !== undefined ? { positional } : {}, }; diff --git a/packages/angular/cli/utilities/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts index 09822cef7730..727ce7836065 100644 --- a/packages/angular/cli/utilities/json-schema_spec.ts +++ b/packages/angular/cli/utilities/json-schema_spec.ts @@ -1,10 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license - * */ import { schema } from '@angular-devkit/core'; import { readFileSync } from 'fs'; diff --git a/packages/angular/cli/utilities/log-file.ts b/packages/angular/cli/utilities/log-file.ts index 4e5d33bb4571..41dc036fc028 100644 --- a/packages/angular/cli/utilities/log-file.ts +++ b/packages/angular/cli/utilities/log-file.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/utilities/package-manager.ts index 77d1318750f9..3163060db231 100644 --- a/packages/angular/cli/utilities/package-manager.ts +++ b/packages/angular/cli/utilities/package-manager.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -8,6 +8,7 @@ import { execSync } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; +import { satisfies, valid } from 'semver'; import { PackageManager } from '../lib/config/schema'; import { getConfiguredPackageManager } from './config'; @@ -54,3 +55,30 @@ export async function getPackageManager(root: string): Promise { // Potentially with a prompt to choose and optionally set as the default. return packageManager || PackageManager.Npm; } + +/** + * Checks if the npm version is a supported 7.x version. If not, display a warning. + */ +export async function ensureCompatibleNpm(root: string): Promise { + if ((await getPackageManager(root)) !== PackageManager.Npm) { + return; + } + + try { + const versionText = execSync('npm --version', {encoding: 'utf8', stdio: 'pipe'}).trim(); + const version = valid(versionText); + if (!version) { + return; + } + + if (satisfies(version, '>=7 <7.5.6')) { + // tslint:disable-next-line: no-console + console.warn( + `npm version ${version} detected.` + + ' When using npm 7 with the Angular CLI, npm version 7.5.6 or higher is recommended.', + ); + } + } catch { + // npm is not installed + } +} diff --git a/packages/angular/cli/utilities/package-metadata.ts b/packages/angular/cli/utilities/package-metadata.ts index 34e5b4e0bf6f..e5c728ec76fb 100644 --- a/packages/angular/cli/utilities/package-metadata.ts +++ b/packages/angular/cli/utilities/package-metadata.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -57,18 +57,20 @@ export interface PackageMetadata { 'dist-tags'?: unknown; } -let npmrc: { [key: string]: string }; +type PackageManagerOptions = Record; + +let npmrc: PackageManagerOptions; function ensureNpmrc(logger: logging.LoggerApi, usingYarn: boolean, verbose: boolean): void { if (!npmrc) { try { npmrc = readOptions(logger, false, verbose); - } catch {} + } catch { } if (usingYarn) { try { npmrc = { ...npmrc, ...readOptions(logger, true, verbose) }; - } catch {} + } catch { } } } } @@ -77,7 +79,7 @@ function readOptions( logger: logging.LoggerApi, yarn = false, showPotentials = false, -): Record { +): PackageManagerOptions { const cwd = process.cwd(); const baseFilename = yarn ? 'yarnrc' : 'npmrc'; const dotFilename = '.' + baseFilename; @@ -107,7 +109,7 @@ function readOptions( logger.info(`Locating potential ${baseFilename} files:`); } - let options: { [key: string]: string } = {}; + const options: PackageManagerOptions = {}; for (const location of [...defaultConfigLocations, ...projectConfigLocations]) { if (existsSync(location)) { if (showPotentials) { @@ -115,17 +117,40 @@ function readOptions( } 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 {} + // Normalize RC options that are needed by 'npm-registry-fetch'. + // See: https://github.com/npm/npm-registry-fetch/blob/ebddbe78a5f67118c1f7af2e02c8a22bcaf9e850/index.js#L99-L126 + const rcConfig: PackageManagerOptions = yarn ? lockfile.parse(data) : ini.parse(data); + for (const [key, value] of Object.entries(rcConfig)) { + switch (key) { + case 'noproxy': + case 'no-proxy': + options['noProxy'] = value; + break; + case 'maxsockets': + options['maxSockets'] = value; + break; + case 'https-proxy': + case 'proxy': + options['proxy'] = value; + break; + case 'strict-ssl': + options['strictSSL'] = value; + break; + case 'local-address': + options['localAddress'] = value; + break; + case 'cafile': + if (typeof value === 'string') { + const cafile = path.resolve(path.dirname(location), value); + try { + options['ca'] = readFileSync(cafile, 'utf8').replace(/\r?\n/g, '\n'); + } catch { } + } + break; + default: + options[key] = value; + break; + } } } else if (showPotentials) { logger.info(`Trying '${location}'...not found.`); @@ -134,8 +159,9 @@ function readOptions( // Substitute any environment variable references for (const key in options) { - if (typeof options[key] === 'string') { - options[key] = options[key].replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || ''); + const value = options[key]; + if (typeof value === 'string') { + options[key] = value.replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || ''); } } diff --git a/packages/angular/cli/utilities/package-tree.ts b/packages/angular/cli/utilities/package-tree.ts index 85505e16531b..d4b7bba35e0b 100644 --- a/packages/angular/cli/utilities/package-tree.ts +++ b/packages/angular/cli/utilities/package-tree.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts index cd133cd0323a..d7a4f8bc1ca8 100644 --- a/packages/angular/cli/utilities/project.ts +++ b/packages/angular/cli/utilities/project.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/cli/utilities/spinner.ts b/packages/angular/cli/utilities/spinner.ts new file mode 100644 index 000000000000..21f3494a98da --- /dev/null +++ b/packages/angular/cli/utilities/spinner.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ora from 'ora'; +import { colors } from './color'; + +export class Spinner { + private readonly spinner: ora.Ora; + + /** When false, only fail messages will be displayed. */ + enabled = true; + + constructor(text?: string) { + this.spinner = ora({ + text, + // The below 2 options are needed because otherwise CTRL+C will be delayed + // when the underlying process is sync. + hideCursor: false, + discardStdin: false, + }); + } + + set text(text: string) { + this.spinner.text = text; + } + + succeed(text?: string): void { + if (this.enabled) { + this.spinner.succeed(text); + } + } + + info(text?: string): void { + this.spinner.info(text); + } + + fail(text?: string): void { + this.spinner.fail(text && colors.redBright(text)); + } + + warn(text?: string): void { + this.spinner.fail(text && colors.yellowBright(text)); + } + + stop(): void { + this.spinner.stop(); + } + + start(text?: string): void { + if (this.enabled) { + this.spinner.start(text); + } + } +} diff --git a/packages/angular/cli/utilities/tty.ts b/packages/angular/cli/utilities/tty.ts index dd5931e26fb6..1e5658ebfd57 100644 --- a/packages/angular/cli/utilities/tty.ts +++ b/packages/angular/cli/utilities/tty.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/pwa/README.md b/packages/angular/pwa/README.md new file mode 100644 index 000000000000..9a2d8181fb8a --- /dev/null +++ b/packages/angular/pwa/README.md @@ -0,0 +1,22 @@ +# `@angular/pwa` + +This is a [schematic](https://angular.io/guide/schematics) for adding +[Progress Web App](https://web.dev/progressive-web-apps/) support to an Angular app. Run the +schematic with the [Angular CLI](https://angular.io/cli): + +```shell +ng add @angular/pwa +``` + +This makes a few changes to your project: + +1. Adds [`@angular/service-worker`](https://npmjs.com/@angular/service-worker) as a dependency. +1. Enables service worker builds in the Angular CLI. +1. Imports and registers the service worker in the app module. +1. Adds a [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest). +1. Updates the `index.html` file to link to the manifest and set theme colors. +1. Adds required icons for the manifest. +1. Creates a config file `ngsw-config.json`, specifying caching behaviors and other settings. + +See [Getting started with service workers](https://angular.io/guide/service-worker-getting-started) +for more information. diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts index 3fac23935f01..17cd5da7d46b 100644 --- a/packages/angular/pwa/pwa/index.ts +++ b/packages/angular/pwa/pwa/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts index b3a54fffcbcc..95bef3167bd0 100644 --- a/packages/angular/pwa/pwa/index_spec.ts +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/builders/all-of.ts b/packages/angular_devkit/architect/builders/all-of.ts index 0b9d958b1f7c..f846fcd32f21 100644 --- a/packages/angular_devkit/architect/builders/all-of.ts +++ b/packages/angular_devkit/architect/builders/all-of.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/builders/concat.ts b/packages/angular_devkit/architect/builders/concat.ts index 816cb2028091..68d21b297968 100644 --- a/packages/angular_devkit/architect/builders/concat.ts +++ b/packages/angular_devkit/architect/builders/concat.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/builders/false.ts b/packages/angular_devkit/architect/builders/false.ts index 3ae22b054e59..7cdb9b9d56eb 100644 --- a/packages/angular_devkit/architect/builders/false.ts +++ b/packages/angular_devkit/architect/builders/false.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/builders/true.ts b/packages/angular_devkit/architect/builders/true.ts index ea617c33fd2b..6b110c012a3c 100644 --- a/packages/angular_devkit/architect/builders/true.ts +++ b/packages/angular_devkit/architect/builders/true.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/node/index.ts b/packages/angular_devkit/architect/node/index.ts index 721047f44093..ab719a3f8ba3 100644 --- a/packages/angular_devkit/architect/node/index.ts +++ b/packages/angular_devkit/architect/node/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/architect/node/node-modules-architect-host.ts b/packages/angular_devkit/architect/node/node-modules-architect-host.ts index 8562b8e07ea6..d4cff7208631 100644 --- a/packages/angular_devkit/architect/node/node-modules-architect-host.ts +++ b/packages/angular_devkit/architect/node/node-modules-architect-host.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -28,17 +28,86 @@ function clone(obj: unknown): unknown { } } -// TODO: create a base class for all workspace related hosts. -export class WorkspaceNodeModulesArchitectHost implements ArchitectHost { - constructor(protected _workspace: workspaces.WorkspaceDefinition, protected _root: string) {} +export interface WorkspaceHost { + getBuilderName(project: string, target: string): Promise; + getMetadata(project: string): Promise; + getOptions(project: string, target: string, configuration?: string): Promise; + hasTarget(project: string, target: string): Promise; +} - async getBuilderNameForTarget(target: Target) { - const targetDefinition = this.findProjectTarget(target); - if (!targetDefinition) { - throw new Error('Project target does not exist.'); +function findProjectTarget( + workspace: workspaces.WorkspaceDefinition, + project: string, + target: string, +): workspaces.TargetDefinition { + const projectDefinition = workspace.projects.get(project); + if (!projectDefinition) { + throw new Error(`Project "${project}" does not exist.`); + } + + const targetDefinition = projectDefinition.targets.get(target); + if (!targetDefinition) { + throw new Error('Project target does not exist.'); + } + + return targetDefinition; +} + +export class WorkspaceNodeModulesArchitectHost implements ArchitectHost { + private workspaceHost: WorkspaceHost; + + constructor(workspaceHost: WorkspaceHost, _root: string); + + constructor(workspace: workspaces.WorkspaceDefinition, _root: string); + + constructor( + workspaceOrHost: workspaces.WorkspaceDefinition | WorkspaceHost, + protected _root: string, + ) { + if ('getBuilderName' in workspaceOrHost) { + this.workspaceHost = workspaceOrHost; + } else { + this.workspaceHost = { + async getBuilderName(project, target) { + const targetDefinition = findProjectTarget(workspaceOrHost, project, target); + + return targetDefinition.builder; + }, + async getOptions(project, target, configuration) { + const targetDefinition = findProjectTarget(workspaceOrHost, project, target); + + if (configuration === undefined) { + return (targetDefinition.options ?? {}) as json.JsonObject; + } + + if (!targetDefinition.configurations?.[configuration]) { + throw new Error(`Configuration '${configuration}' is not set in the workspace.`); + } + + return (targetDefinition.configurations?.[configuration] ?? {}) as json.JsonObject; + }, + async getMetadata(project) { + const projectDefinition = workspaceOrHost.projects.get(project); + if (!projectDefinition) { + throw new Error(`Project "${project}" does not exist.`); + } + + return ({ + root: projectDefinition.root, + sourceRoot: projectDefinition.sourceRoot, + prefix: projectDefinition.prefix, + ...(clone(projectDefinition.extensions) as {}), + } as unknown) as json.JsonObject; + }, + async hasTarget(project, target) { + return !!workspaceOrHost.projects.get(project)?.targets.has(target); + }, + }; } + } - return targetDefinition.builder; + async getBuilderNameForTarget(target: Target) { + return this.workspaceHost.getBuilderName(target.project, target.target); } /** @@ -95,49 +164,28 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost { - const targetSpec = this.findProjectTarget(target); - if (targetSpec === undefined) { + if (!(await this.workspaceHost.hasTarget(target.project, target.target))) { return null; } - let additionalOptions = {}; + let options = await this.workspaceHost.getOptions(target.project, target.target); if (target.configuration) { - const configurations = target.configuration.split(',').map(c => c.trim()); + const configurations = target.configuration.split(',').map((c) => c.trim()); for (const configuration of configurations) { - if (!(targetSpec['configurations'] && targetSpec['configurations'][configuration])) { - throw new Error(`Configuration '${configuration}' is not set in the workspace.`); - } else { - additionalOptions = { - ...additionalOptions, - ...targetSpec['configurations'][configuration], - }; - } + options = { + ...options, + ...await this.workspaceHost.getOptions(target.project, target.target, configuration), + }; } } - const options = { - ...targetSpec['options'], - ...additionalOptions, - }; - return clone(options) as json.JsonObject; } async getProjectMetadata(target: Target | string): Promise { const projectName = typeof target === 'string' ? target : target.project; - - const projectDefinition = this._workspace.projects.get(projectName); - if (!projectDefinition) { - throw new Error(`Project "${projectName}" does not exist.`); - } - - const metadata = ({ - root: projectDefinition.root, - sourceRoot: projectDefinition.sourceRoot, - prefix: projectDefinition.prefix, - ...clone(projectDefinition.extensions) as {}, - } as unknown) as json.JsonObject; + const metadata = this.workspaceHost.getMetadata(projectName); return metadata; } @@ -150,13 +198,4 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost=3.9 < 4.1" + "typescript": "~4.0.0 || ~4.1.0" }, "peerDependenciesMeta": { "@angular/localize": { "optional": true }, + "@angular/service-worker": { + "optional": true + }, "karma": { "optional": true }, @@ -99,6 +108,9 @@ "protractor": { "optional": true }, + "tailwindcss": { + "optional": true + }, "tslint": { "optional": true } diff --git a/packages/angular_devkit/build_angular/plugins/karma.ts b/packages/angular_devkit/build_angular/plugins/karma.ts index 7a5803a55286..18409cffe9a9 100644 --- a/packages/angular_devkit/build_angular/plugins/karma.ts +++ b/packages/angular_devkit/build_angular/plugins/karma.ts @@ -1,9 +1,9 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -module.exports = require('../src/webpack/plugins/karma'); +module.exports = require('../src/webpack/plugins/karma/karma'); diff --git a/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts b/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts index 89d054d43e7d..4e4ef3d53296 100644 --- a/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts +++ b/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -9,6 +9,8 @@ import { Architect } from '@angular-devkit/architect'; import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core'; import * as express from 'express'; // tslint:disable-line:no-implicit-dependencies +import * as http from 'http'; +import { AddressInfo } from 'net'; import { createArchitect, host } from '../test-utils'; describe('AppShell Builder', () => { @@ -22,6 +24,9 @@ describe('AppShell Builder', () => { afterEach(async () => host.restore().toPromise()); const appShellRouteFiles = { + 'src/styles.css': ` + p { color: #000 } + `, 'src/app/app-shell/app-shell.component.html': `

app-shell works! @@ -248,18 +253,44 @@ describe('AppShell Builder', () => { // Serve the app using a simple static server. const app = express(); app.use('/', express.static(getSystemPath(join(host.root(), 'dist')) + '/')); - const server = app.listen(4200); + const server = await new Promise((resolve) => { + const innerServer = app.listen(0, 'localhost', () => resolve(innerServer)); + }); + try { + const serverPort = (server.address() as AddressInfo).port; + // Load app in protractor, then check service worker status. + const protractorRun = await architect.scheduleTarget( + { project: 'app-e2e', target: 'e2e' }, + { baseUrl: `http://localhost:${serverPort}/`, devServerTarget: '' }, + ); + + const protractorOutput = await protractorRun.result; + await protractorRun.stop(); + + expect(protractorOutput.success).toBe(true); + } finally { + // Close the express server. + await new Promise((resolve) => server.close(() => resolve())); + } + }); - // Load app in protractor, then check service worker status. - const protractorRun = await architect.scheduleTarget( - { project: 'app-e2e', target: 'e2e' }, - { devServerTarget: undefined } as {}, - ); - const protractorOutput = await protractorRun.result; - await protractorRun.stop(); - expect(protractorOutput.success).toBe(true); + it('critical CSS is inlined', async () => { + host.writeMultipleFiles(appShellRouteFiles); + const overrides = { + route: 'shell', + browserTarget: 'app:build:production,inline-critical-css', + }; + + const run = await architect.scheduleTarget(target, overrides); + const output = await run.result; + await run.stop(); - // Close the express server. - server.close(); + expect(output.success).toBe(true); + const fileName = 'dist/index.html'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + + expect(content).toContain('app-shell works!'); + expect(content).toContain('p{color:#000}'); + expect(content).toMatch(//); }); }); diff --git a/packages/angular_devkit/build_angular/src/app-shell/index.ts b/packages/angular_devkit/build_angular/src/app-shell/index.ts index f7996f0e68af..e20d01dad734 100644 --- a/packages/angular_devkit/build_angular/src/app-shell/index.ts +++ b/packages/angular_devkit/build_angular/src/app-shell/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -12,13 +12,16 @@ import { targetFromTargetString, } from '@angular-devkit/architect'; import { JsonObject, normalize, resolve } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; import * as fs from 'fs'; import * as path from 'path'; import { BrowserBuilderOutput } from '../browser'; import { Schema as BrowserBuilderSchema } from '../browser/schema'; import { ServerBuilderOutput } from '../server'; +import { normalizeOptimization } from '../utils'; +import { readFile, writeFile } from '../utils/fs'; +import { InlineCriticalCssProcessor } from '../utils/index-file/inline-critical-css'; import { augmentAppWithServiceWorker } from '../utils/service-worker'; +import { Spinner } from '../utils/spinner'; import { Schema as BuildWebpackAppShellSchema } from './schema'; async function _renderUniversal( @@ -26,22 +29,23 @@ async function _renderUniversal( context: BuilderContext, browserResult: BrowserBuilderOutput, serverResult: ServerBuilderOutput, + spinner: Spinner, ): Promise { // Get browser target options. const browserTarget = targetFromTargetString(options.browserTarget); - const rawBrowserOptions = await context.getTargetOptions(browserTarget); + const rawBrowserOptions = (await context.getTargetOptions(browserTarget)) as JsonObject & BrowserBuilderSchema; const browserBuilderName = await context.getBuilderNameForTarget(browserTarget); const browserOptions = await context.validateOptions( rawBrowserOptions, browserBuilderName, ); + // Initialize zone.js const root = context.workspaceRoot; const zonePackage = require.resolve('zone.js', { paths: [root] }); await import(zonePackage); - const host = new NodeJsSyncHost(); const projectName = context.target && context.target.project; if (!projectName) { throw new Error('The builder requires a target.'); @@ -53,10 +57,18 @@ async function _renderUniversal( normalize((projectMetadata.root as string) || ''), ); + const { styles } = normalizeOptimization(browserOptions.optimization); + const inlineCriticalCssProcessor = styles.inlineCritical + ? new InlineCriticalCssProcessor({ + minify: styles.minify, + deployUrl: browserOptions.deployUrl, + }) + : undefined; + for (const outputPath of browserResult.outputPaths) { const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); const browserIndexOutputPath = path.join(outputPath, 'index.html'); - const indexHtml = fs.readFileSync(browserIndexOutputPath, 'utf8'); + const indexHtml = await readFile(browserIndexOutputPath, 'utf8'); const serverBundlePath = await _getServerModuleBundlePath(options, context, serverResult, localeDirectory); const { @@ -85,17 +97,28 @@ async function _renderUniversal( url: options.route, }; - const html = await renderModuleFn(AppServerModuleDef, renderOpts); + let html = await renderModuleFn(AppServerModuleDef, renderOpts); // Overwrite the client index file. const outputIndexPath = options.outputIndexPath ? path.join(root, options.outputIndexPath) : browserIndexOutputPath; - fs.writeFileSync(outputIndexPath, html); + if (inlineCriticalCssProcessor) { + const { content, warnings, errors } = await inlineCriticalCssProcessor.process(html, { outputPath }); + html = content; + + if (warnings.length || errors.length) { + spinner.stop(); + warnings.forEach(m => context.logger.warn(m)); + errors.forEach(m => context.logger.error(m)); + spinner.start(); + } + } + + await writeFile(outputIndexPath, html); if (browserOptions.serviceWorker) { await augmentAppWithServiceWorker( - host, normalize(root), projectRoot, normalize(outputPath), @@ -116,24 +139,23 @@ async function _getServerModuleBundlePath( ) { if (options.appModuleBundle) { return path.join(context.workspaceRoot, options.appModuleBundle); - } else { - const { baseOutputPath = '' } = serverResult; - const outputPath = path.join(baseOutputPath, browserLocaleDirectory); + } - if (!fs.existsSync(outputPath)) { - throw new Error(`Could not find server output directory: ${outputPath}.`); - } + const { baseOutputPath = '' } = serverResult; + const outputPath = path.join(baseOutputPath, browserLocaleDirectory); - const files = fs.readdirSync(outputPath, 'utf8'); - const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?(?:bundle\.)?js$/; - const maybeMain = files.filter(x => re.test(x))[0]; + if (!fs.existsSync(outputPath)) { + throw new Error(`Could not find server output directory: ${outputPath}.`); + } - if (!maybeMain) { - throw new Error('Could not find the main bundle.'); - } else { - return path.join(outputPath, maybeMain); - } + const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?js$/; + const maybeMain = fs.readdirSync(outputPath).find(x => re.test(x)); + + if (!maybeMain) { + throw new Error('Could not find the main bundle.'); } + + return path.join(outputPath, maybeMain); } async function _appShellBuilder( @@ -145,14 +167,22 @@ async function _appShellBuilder( // Never run the browser target in watch mode. // If service worker is needed, it will be added in _renderUniversal(); + const browserOptions = (await context.getTargetOptions(browserTarget)) as JsonObject & BrowserBuilderSchema; + + const optimization = normalizeOptimization(browserOptions.optimization); + optimization.styles.inlineCritical = false; + const browserTargetRun = await context.scheduleTarget(browserTarget, { watch: false, serviceWorker: false, + optimization: (optimization as unknown as JsonObject), }); const serverTargetRun = await context.scheduleTarget(serverTarget, { watch: false, }); + let spinner: Spinner | undefined; + try { const [browserResult, serverResult] = await Promise.all([ browserTargetRun.result as unknown as BrowserBuilderOutput, @@ -165,8 +195,15 @@ async function _appShellBuilder( return serverResult; } - return await _renderUniversal(options, context, browserResult, serverResult); + spinner = new Spinner(); + spinner.start('Generating application shell...'); + const result = await _renderUniversal(options, context, browserResult, serverResult, spinner); + spinner.succeed('Application shell generation complete.'); + + return result; } catch (err) { + spinner?.fail('Application shell generation failed.'); + return { success: false, error: err.message }; } finally { // Just be good citizens and stop those jobs. diff --git a/packages/angular_devkit/build_angular/src/app-shell/schema.json b/packages/angular_devkit/build_angular/src/app-shell/schema.json index b89b795265ab..3adcc32a7521 100644 --- a/packages/angular_devkit/build_angular/src/app-shell/schema.json +++ b/packages/angular_devkit/build_angular/src/app-shell/schema.json @@ -6,12 +6,12 @@ "properties": { "browserTarget": { "type": "string", - "description": "Target to build.", + "description": "A browser builder target use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "serverTarget": { "type": "string", - "description": "Server target to use for rendering the app shell.", + "description": "A server builder target use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, "appModuleBundle": { diff --git a/packages/angular_devkit/build_angular/src/babel-bazel.d.ts b/packages/angular_devkit/build_angular/src/babel-bazel.d.ts new file mode 100644 index 000000000000..fe265e4d3b23 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel-bazel.d.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Workaround for https://github.com/bazelbuild/rules_nodejs/issues/1033 +// Alternative approach instead of https://github.com/angular/angular/pull/33226 +declare module '@babel/core' { + export * from '@types/babel__core'; +} +declare module '@babel/generator' { + export { default } from '@types/babel__generator'; +} +declare module '@babel/traverse' { + export { default } from '@types/babel__traverse'; +} +declare module '@babel/template' { + export { default } from '@types/babel__template'; +} diff --git a/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts new file mode 100644 index 000000000000..1bc5380f631f --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +declare module 'babel-loader' { + type BabelLoaderCustomizer = ( + babel: typeof import('@babel/core'), + ) => { + customOptions?( + this: import('webpack').loader.LoaderContext, + loaderOptions: Record, + loaderArguments: { source: string; map?: unknown }, + ): Promise<{ custom?: T; loader: Record }>; + config?( + this: import('webpack').loader.LoaderContext, + configuration: import('@babel/core').PartialConfig, + loaderArguments: { source: string; map?: unknown; customOptions: T }, + ): import('@babel/core').TransformOptions; + }; + function custom(customizer: BabelLoaderCustomizer): import('webpack').loader.Loader; +} diff --git a/packages/angular_devkit/build_angular/src/babel/presets/application.ts b/packages/angular_devkit/build_angular/src/babel/presets/application.ts new file mode 100644 index 000000000000..98f2d67863d4 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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'; +import * as path from 'path'; + +export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: string) => void; +export interface ApplicationPresetOptions { + i18n?: { + locale: string; + missingTranslationBehavior?: 'error' | 'warning' | 'ignore'; + translation?: unknown; + }; + + angularLinker?: boolean; + + forceES5?: boolean; + forceAsyncTransformation?: boolean; + + diagnosticReporter?: DiagnosticReporter; +} + +type I18nDiagnostics = import('@angular/localize/src/tools/src/diagnostics').Diagnostics; +function createI18nDiagnostics(reporter: DiagnosticReporter | undefined): I18nDiagnostics { + // Babel currently is synchronous so import cannot be used + const diagnostics: I18nDiagnostics = new (require('@angular/localize/src/tools/src/diagnostics').Diagnostics)(); + + if (!reporter) { + return diagnostics; + } + + const baseAdd = diagnostics.add; + diagnostics.add = function (type, message, ...args) { + if (type !== 'ignore') { + baseAdd.call(diagnostics, type, message, ...args); + reporter(type, message); + } + }; + + const baseError = diagnostics.error; + diagnostics.error = function (message, ...args) { + baseError.call(diagnostics, message, ...args); + reporter('error', message); + }; + + const baseWarn = diagnostics.warn; + diagnostics.warn = function (message, ...args) { + baseWarn.call(diagnostics, message, ...args); + reporter('warning', message); + }; + + const baseMerge = diagnostics.merge; + diagnostics.merge = function (other, ...args) { + baseMerge.call(diagnostics, other, ...args); + for (const diagnostic of other.messages) { + reporter(diagnostic.type, diagnostic.message); + } + }; + + return diagnostics; +} + +function createI18nPlugins( + locale: string, + translation: unknown | undefined, + missingTranslationBehavior: 'error' | 'warning' | 'ignore', + diagnosticReporter: DiagnosticReporter | undefined, +) { + const diagnostics = createI18nDiagnostics(diagnosticReporter); + const plugins = []; + + if (translation) { + const { + makeEs2015TranslatePlugin, + } = require('@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin'); + plugins.push( + makeEs2015TranslatePlugin(diagnostics, translation, { + missingTranslation: missingTranslationBehavior, + }), + ); + + const { + makeEs5TranslatePlugin, + } = require('@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin'); + plugins.push( + makeEs5TranslatePlugin(diagnostics, translation, { + missingTranslation: missingTranslationBehavior, + }), + ); + } + + const { + makeLocalePlugin, + } = require('@angular/localize/src/tools/src/translate/source_files/locale_plugin'); + plugins.push(makeLocalePlugin(locale)); + + return plugins; +} + +function createNgtscLogger( + reporter: DiagnosticReporter | undefined, +): import('@angular/compiler-cli/src/ngtsc/logging').Logger { + return { + level: 1, // Info level + debug(...args: string[]) {}, + info(...args: string[]) { + reporter?.('info', args.join()); + }, + warn(...args: string[]) { + reporter?.('warning', args.join()); + }, + error(...args: string[]) { + reporter?.('error', args.join()); + }, + }; +} + +export default function (api: unknown, options: ApplicationPresetOptions) { + const presets = []; + const plugins = []; + let needRuntimeTransform = false; + + if (options.angularLinker) { + // Babel currently is synchronous so import cannot be used + const { + createEs2015LinkerPlugin, + } = require('@angular/compiler-cli/linker/babel'); + + plugins.push(createEs2015LinkerPlugin({ + logger: createNgtscLogger(options.diagnosticReporter), + fileSystem: { + resolve: path.resolve, + exists: fs.existsSync, + dirname: path.dirname, + relative: path.relative, + readFile: fs.readFileSync, + }, + })); + } + + if (options.forceES5) { + presets.push([ + require('@babel/preset-env').default, + { + bugfixes: true, + modules: false, + // Comparable behavior to tsconfig target of ES5 + targets: { ie: 9 }, + exclude: ['transform-typeof-symbol'], + }, + ]); + needRuntimeTransform = true; + } + + if (options.i18n) { + const { locale, missingTranslationBehavior, translation } = options.i18n; + const i18nPlugins = createI18nPlugins( + locale, + translation, + missingTranslationBehavior || 'ignore', + options.diagnosticReporter, + ); + + plugins.push(...i18nPlugins); + } + + if (options.forceAsyncTransformation) { + // Always transform async/await to support Zone.js + plugins.push(require('@babel/plugin-transform-async-to-generator').default); + needRuntimeTransform = true; + } + + if (needRuntimeTransform) { + // Babel equivalent to TypeScript's `importHelpers` option + plugins.push([ + require('@babel/plugin-transform-runtime').default, + { + useESModules: true, + version: require('@babel/runtime/package.json').version, + absoluteRuntime: path.dirname(require.resolve('@babel/runtime/package.json')), + }, + ]); + } + + return { presets, plugins }; +} diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts new file mode 100644 index 000000000000..964103e89a62 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { custom } from 'babel-loader'; +import { ScriptTarget } from 'typescript'; +import { ApplicationPresetOptions } from './presets/application'; + +interface AngularCustomOptions { + forceAsyncTransformation: boolean; + forceES5: boolean; + shouldLink: boolean; + i18n: ApplicationPresetOptions['i18n']; +} + +/** + * Cached linker check utility function + * + * If undefined, not yet been imported + * If null, attempted import failed and no linker support + * If function, import succeeded and linker supported + */ +let needsLinking: undefined | null | typeof import('@angular/compiler-cli/linker').needsLinking; + +async function checkLinking( + path: string, + source: string, +): Promise<{ hasLinkerSupport?: boolean; requiresLinking: boolean }> { + // @angular/core and @angular/compiler will cause false positives + // Also, TypeScript files do not require linking + if (/[\\\/]@angular[\\\/](?:compiler|core)|\.tsx?$/.test(path)) { + return { requiresLinking: false }; + } + + if (needsLinking !== null) { + try { + if (needsLinking === undefined) { + needsLinking = (await import('@angular/compiler-cli/linker')).needsLinking; + } + + // If the linker entry point is present then there is linker support + return { hasLinkerSupport: true, requiresLinking: needsLinking(path, source) }; + } catch { + needsLinking = null; + } + } + + // Fallback for Angular versions less than 11.1.0 with no linker support. + // This information is used to issue errors if a partially compiled library is used when unsupported. + return { + hasLinkerSupport: false, + requiresLinking: + source.includes('ɵɵngDeclareDirective') || source.includes('ɵɵngDeclareComponent'), + }; +} + +export default custom(() => { + const baseOptions = Object.freeze({ + babelrc: false, + configFile: false, + compact: false, + cacheCompression: false, + sourceType: 'unambiguous', + inputSourceMap: false, + }); + + return { + async customOptions({ i18n, scriptTarget, ...rawOptions }, { source }) { + // Must process file if plugins are added + let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0; + + const customOptions: AngularCustomOptions = { + forceAsyncTransformation: false, + forceES5: false, + shouldLink: false, + i18n: undefined, + }; + + // Analyze file for linking + const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source); + if (requiresLinking && !hasLinkerSupport) { + // Cannot link if there is no linker support + this.emitError( + 'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.', + ); + } else { + customOptions.shouldLink = requiresLinking; + } + shouldProcess ||= customOptions.shouldLink; + + // Analyze for ES target processing + const esTarget = scriptTarget as ScriptTarget | undefined; + if (esTarget !== undefined) { + if (esTarget < ScriptTarget.ES2015) { + // TypeScript files will have already been downlevelled + customOptions.forceES5 = !/\.tsx?$/.test(this.resourcePath); + } else if (esTarget >= ScriptTarget.ES2017) { + customOptions.forceAsyncTransformation = !/[\\\/]fesm2015[\\\/]/.test(this.resourcePath) && source.includes('async'); + } + shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5; + } + + // Analyze for i18n inlining + if ( + i18n && + !/[\\\/]@angular[\\\/](?:compiler|localize)/.test(this.resourcePath) && + source.includes('$localize') + ) { + customOptions.i18n = i18n as ApplicationPresetOptions['i18n']; + shouldProcess = true; + } + + // Add provided loader options to default base options + const loaderOptions: Record = { + ...baseOptions, + ...rawOptions, + cacheIdentifier: JSON.stringify({ + buildAngular: require('../../package.json').version, + customOptions, + baseOptions, + rawOptions, + }), + }; + + // Skip babel processing if no actions are needed + if (!shouldProcess) { + // Force the current file to be ignored + loaderOptions.ignore = [() => true]; + } + + return { custom: customOptions, loader: loaderOptions }; + }, + config(configuration, { customOptions }) { + return { + ...configuration.options, + // Workaround for https://github.com/babel/babel-loader/pull/896 is available + // Delete once the above PR is released + inputSourceMap: (configuration.options.inputSourceMap || false as {}), // Typings are not correct + presets: [ + ...(configuration.options.presets || []), + [ + require('./presets/application').default, + { + angularLinker: customOptions.shouldLink, + forceES5: customOptions.forceES5, + forceAsyncTransformation: customOptions.forceAsyncTransformation, + i18n: customOptions.i18n, + diagnosticReporter: (type, message) => { + switch (type) { + case 'error': + this.emitError(message); + break; + case 'info': + // Webpack does not currently have an informational diagnostic + case 'warning': + this.emitWarning(message); + break; + } + }, + } as ApplicationPresetOptions, + ], + ], + }; + }, + }; +}); diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index f71185d4644b..f406a666fa4c 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -1,16 +1,14 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { EmittedFiles, WebpackLoggingCallback, runWebpack } from '@angular-devkit/build-webpack'; -import { getSystemPath, json, normalize, resolve, tags, virtualFs } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { getSystemPath, json, normalize, resolve, tags } from '@angular-devkit/core'; import * as fs from 'fs'; -import * as ora from 'ora'; import * as path from 'path'; import { Observable, from } from 'rxjs'; import { concatMap, map, switchMap } from 'rxjs/operators'; @@ -19,7 +17,6 @@ import * as webpack from 'webpack'; import { ExecutionTransformer } from '../transforms'; import { BuildBrowserFeatures, - NormalizedBrowserBuilderSchema, deleteOutputDir, normalizeAssetPatterns, normalizeOptimization, @@ -33,14 +30,13 @@ import { findCachePath } from '../utils/cache-path'; import { colors } from '../utils/color'; import { copyAssets } from '../utils/copy-assets'; import { cachingDisabled } from '../utils/environment-options'; +import { mkdir, writeFile } from '../utils/fs'; import { i18nInlineEmittedFiles } from '../utils/i18n-inlining'; import { I18nOptions } from '../utils/i18n-options'; -import { getHtmlTransforms } from '../utils/index-file/transforms'; -import { - IndexHtmlTransform, - writeIndexHtml, -} from '../utils/index-file/write-index-html'; +import { FileInfo } from '../utils/index-file/augment-index-html'; +import { IndexHtmlGenerator, IndexHtmlTransform } from '../utils/index-file/index-html-generator'; import { ensureOutputPaths } from '../utils/output-paths'; +import { generateEntryPoints } from '../utils/package-chunk-sort'; import { InlineOptions, ProcessBundleFile, @@ -49,14 +45,13 @@ import { } from '../utils/process-bundle'; import { readTsconfig } from '../utils/read-tsconfig'; import { augmentAppWithServiceWorker } from '../utils/service-worker'; +import { Spinner } from '../utils/spinner'; import { assertCompatibleAngularVersion } from '../utils/version'; import { - BrowserWebpackConfigOptions, generateI18nBrowserWebpackConfigFromContext, getIndexInputFile, getIndexOutputFile, } from '../utils/webpack-browser-config'; -import { isWebpackFiveOrHigher } from '../utils/webpack-version'; import { getAotConfig, getBrowserConfig, @@ -65,20 +60,19 @@ import { getStatsConfig, getStylesConfig, getWorkerConfig, - normalizeExtraEntryPoints, } from '../webpack/configs'; import { NgBuildAnalyticsPlugin } from '../webpack/plugins/analytics'; import { markAsyncChunksNonInitial } from '../webpack/utils/async-chunks'; +import { normalizeExtraEntryPoints } from '../webpack/utils/helpers'; import { BundleStats, - createWebpackLoggingCallback, - generateBuildStats, - generateBuildStatsTable, + ChunkType, generateBundleStats, statsErrorsToString, statsHasErrors, statsHasWarnings, statsWarningsToString, + webpackStatsLogger, } from '../webpack/utils/stats'; import { Schema as BrowserBuilderSchema } from './schema'; @@ -94,40 +88,7 @@ export type BrowserBuilderOutput = json.JsonObject & outputPath: string; }; -// todo: the below should be cleaned once dev-server support the new i18n -interface ConfigFromContextReturn { - config: webpack.Configuration; - projectRoot: string; - projectSourceRoot?: string; - i18n: I18nOptions; -} - -export async function buildBrowserWebpackConfigFromContext( - options: BrowserBuilderSchema, - context: BuilderContext, - host: virtualFs.Host = new NodeJsSyncHost(), - extraBuildOptions: Partial = {}, -): Promise { - const webpackPartialGenerator = (wco: BrowserWebpackConfigOptions) => [ - getCommonConfig(wco), - getBrowserConfig(wco), - getStylesConfig(wco), - getStatsConfig(wco), - getAnalyticsConfig(wco, context), - getCompilerConfig(wco), - wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}, - ]; - - return generateI18nBrowserWebpackConfigFromContext( - options, - context, - webpackPartialGenerator, - host, - extraBuildOptions, - ); -} - -function getAnalyticsConfig( +export function getAnalyticsConfig( wco: WebpackConfigOptions, context: BuilderContext, ): webpack.Configuration { @@ -154,7 +115,7 @@ function getAnalyticsConfig( return {}; } -function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration { +export function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration { if (wco.buildOptions.main || wco.buildOptions.polyfills) { return wco.buildOptions.aot ? getAotConfig(wco) : getNonAotConfig(wco); } @@ -165,8 +126,7 @@ function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration { async function initialize( options: BrowserBuilderSchema, context: BuilderContext, - host: virtualFs.Host, - differentialLoadingMode: boolean, + differentialLoadingNeeded: boolean, webpackConfigurationTransform?: ExecutionTransformer, ): Promise<{ config: webpack.Configuration; @@ -179,27 +139,30 @@ async function initialize( // Assets are processed directly by the builder except when watching const adjustedOptions = options.watch ? options : { ...options, assets: [] }; - // TODO_WEBPACK_5: Investigate build/serve issues with the `license-webpack-plugin` package - if (adjustedOptions.extractLicenses && isWebpackFiveOrHigher()) { - adjustedOptions.extractLicenses = false; - context.logger.warn( - 'Warning: License extraction is currently disabled when using Webpack 5. ' + - 'This is temporary and will be corrected in a future update.', - ); - } - const { config, projectRoot, projectSourceRoot, i18n, - } = await buildBrowserWebpackConfigFromContext(adjustedOptions, context, host, { differentialLoadingMode }); + } = await generateI18nBrowserWebpackConfigFromContext( + adjustedOptions, + context, + wco => [ + getCommonConfig(wco), + getBrowserConfig(wco), + getStylesConfig(wco), + getStatsConfig(wco), + getAnalyticsConfig(wco, context), + getCompilerConfig(wco), + wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {}, + ], + { differentialLoadingNeeded }, + ); // Validate asset option values if processed directly if (options.assets?.length && !adjustedOptions.assets?.length) { normalizeAssetPatterns( options.assets, - new virtualFs.SyncDelegateHost(host), normalize(context.workspaceRoot), normalize(projectRoot), projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot), @@ -232,7 +195,6 @@ export function buildWebpackBrowser( indexHtml?: IndexHtmlTransform; } = {}, ): Observable { - const host = new NodeJsSyncHost(); const root = normalize(context.workspaceRoot); const projectName = context.target?.project; @@ -251,14 +213,13 @@ export function buildWebpackBrowser( switchMap(async projectMetadata => { const sysProjectRoot = getSystemPath( resolve(normalize(context.workspaceRoot), - normalize((projectMetadata.root as string) ?? '')), + normalize((projectMetadata.root as string) ?? '')), ); const { options: compilerOptions } = readTsconfig(options.tsConfig, context.workspaceRoot); const target = compilerOptions.target || ScriptTarget.ES5; const buildBrowserFeatures = new BuildBrowserFeatures(sysProjectRoot); const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(target); - const differentialLoadingMode = !options.watch && isDifferentialLoadingNeeded; if (target > ScriptTarget.ES2015 && isDifferentialLoadingNeeded) { context.logger.warn(tags.stripIndent` @@ -275,14 +236,14 @@ export function buildWebpackBrowser( (hasIE9 ? 'IE 9' + (hasIE10 ? ' & ' : '') : '') + (hasIE10 ? 'IE 10' : ''); context.logger.warn( `Warning: Support was requested for ${browsers} in the project's browserslist configuration. ` + - (hasIE9 && hasIE10 ? 'These browsers are' : 'This browser is') + - ' no longer officially supported with Angular v11 and higher.' + - '\nFor additional information: https://v10.angular.io/guide/deprecations#ie-9-10-and-mobile', + (hasIE9 && hasIE10 ? 'These browsers are' : 'This browser is') + + ' no longer officially supported with Angular v11 and higher.' + + '\nFor additional information: https://v10.angular.io/guide/deprecations#ie-9-10-and-mobile', ); } return { - ...(await initialize(options, context, host, differentialLoadingMode, transforms.webpackConfiguration)), + ...(await initialize(options, context, isDifferentialLoadingNeeded, transforms.webpackConfiguration)), buildBrowserFeatures, isDifferentialLoadingNeeded, target, @@ -290,39 +251,40 @@ export function buildWebpackBrowser( }), // tslint:disable-next-line: no-big-function switchMap(({ config, projectRoot, projectSourceRoot, i18n, buildBrowserFeatures, isDifferentialLoadingNeeded, target }) => { - const useBundleDownleveling = isDifferentialLoadingNeeded && !options.watch; - const startTime = Date.now(); const normalizedOptimization = normalizeOptimization(options.optimization); - const indexTransforms = getHtmlTransforms( - normalizedOptimization, - buildBrowserFeatures, - transforms.indexHtml, - ); return runWebpack(config, context, { webpackFactory: require('webpack') as typeof webpack, - logging: - transforms.logging || - (useBundleDownleveling - ? () => { } - : createWebpackLoggingCallback(!!options.verbose, context.logger)), + logging: transforms.logging || ( + (stats, config) => { + if (options.verbose) { + context.logger.info(stats.toString(config.stats)); + } + } + ), }).pipe( // tslint:disable-next-line: no-big-function concatMap(async buildEvent => { + const spinner = new Spinner(); + spinner.enabled = options.progress !== false; + const { webpackStats: webpackRawStats, success, emittedFiles = [] } = buildEvent; if (!webpackRawStats) { throw new Error('Webpack stats build result is required.'); } // Fix incorrectly set `initial` value on chunks. - const extraEntryPoints = normalizeExtraEntryPoints(options.styles || [], 'styles') - .concat(normalizeExtraEntryPoints(options.scripts || [], 'scripts')); + const extraEntryPoints = [ + ...normalizeExtraEntryPoints(options.styles || [], 'styles'), + ...normalizeExtraEntryPoints(options.scripts || [], 'scripts'), + ]; + const webpackStats = { ...webpackRawStats, chunks: markAsyncChunksNonInitial(webpackRawStats, extraEntryPoints), }; - if (!success && useBundleDownleveling) { + if (!success) { // If using bundle downleveling then there is only one build // If it fails show any diagnostic messages and bail if (statsHasWarnings(webpackStats)) { @@ -333,7 +295,9 @@ export function buildWebpackBrowser( } return { success }; - } else if (success) { + } else { + const processResults: ProcessBundleResult[] = []; + const bundleInfoStats: BundleStats[] = []; outputPaths = ensureOutputPaths(baseOutputPath, i18n); let noModuleFiles: EmittedFiles[] | undefined; @@ -482,7 +446,6 @@ export function buildWebpackBrowser( const processActions: typeof actions = []; let processRuntimeAction: ProcessBundleOptions | undefined; - const processResults: ProcessBundleResult[] = []; for (const action of actions) { // If SRI is enabled always process the runtime bundle // Lazy route integrity values are stored in the runtime bundle @@ -500,7 +463,7 @@ export function buildWebpackBrowser( // Execute the bundle processing actions try { - const dlSpinner = ora('Generating ES5 bundles for differential loading...').start(); + spinner.start('Generating ES5 bundles for differential loading...'); for await (const result of executor.processAll(processActions)) { processResults.push(result); } @@ -517,11 +480,10 @@ export function buildWebpackBrowser( ); } - dlSpinner.succeed('ES5 bundle generation complete.'); + spinner.succeed('ES5 bundle generation complete.'); if (i18n.shouldInline) { - const spinner = ora('Generating localized bundles...').start(); - + spinner.start('Generating localized bundles...'); const inlineActions: InlineOptions[] = []; const processedFiles = new Set(); for (const result of processResults) { @@ -599,13 +561,13 @@ export function buildWebpackBrowser( '', ); } catch (err) { - spinner.fail(colors.redBright('Localized bundle generation failed.')); + spinner.fail('Localized bundle generation failed.'); return { success: false, error: mapErrorToMessage(err) }; } if (hasErrors) { - spinner.fail(colors.redBright('Localized bundle generation failed.')); + spinner.fail('Localized bundle generation failed.'); } else { spinner.succeed('Localized bundle generation complete.'); } @@ -617,35 +579,15 @@ export function buildWebpackBrowser( } finally { executor.stop(); } - - type ArrayElement = A extends ReadonlyArray ? T : never; - function generateBundleInfoStats( - bundle: ProcessBundleFile, - chunk: ArrayElement | undefined, - ): BundleStats { - return generateBundleStats( - { - size: bundle.size, - files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], - names: chunk?.names, - entry: !!chunk?.names.includes('runtime'), - initial: !!chunk?.initial, - rendered: true, - }, - true, - ); - } - - const bundleInfoStats: BundleStats[] = []; for (const result of processResults) { const chunk = webpackStats.chunks?.find((chunk) => chunk.id.toString() === result.name); if (result.original) { - bundleInfoStats.push(generateBundleInfoStats(result.original, chunk)); + bundleInfoStats.push(generateBundleInfoStats(result.original, chunk, 'modern')); } if (result.downlevel) { - bundleInfoStats.push(generateBundleInfoStats(result.downlevel, chunk)); + bundleInfoStats.push(generateBundleInfoStats(result.downlevel, chunk, 'legacy')); } } @@ -654,43 +596,7 @@ export function buildWebpackBrowser( ) || []; for (const chunk of unprocessedChunks) { const asset = webpackStats.assets?.find(a => a.name === chunk.files[0]); - bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset?.size }, true)); - } - - context.logger.info( - '\n' + - generateBuildStatsTable(bundleInfoStats, colors.enabled) + - '\n\n' + - generateBuildStats( - webpackStats?.hash || '', - Date.now() - startTime, - true, - ), - ); - - // Check for budget errors and display them to the user. - const budgets = options.budgets || []; - const budgetFailures = checkBudgets(budgets, webpackStats, processResults); - for (const { severity, message } of budgetFailures) { - switch (severity) { - case ThresholdSeverity.Warning: - webpackStats.warnings.push(message); - break; - case ThresholdSeverity.Error: - webpackStats.errors.push(message); - break; - default: - assertNever(severity); - } - } - - if (statsHasWarnings(webpackStats)) { - context.logger.warn(statsWarningsToString(webpackStats, { colors: true })); - } - if (statsHasErrors(webpackStats)) { - context.logger.error(statsErrorsToString(webpackStats, { colors: true })); - - return { success: false }; + bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset?.size })); } } else { files = emittedFiles.filter(x => x.name !== 'polyfills-es5'); @@ -714,72 +620,126 @@ export function buildWebpackBrowser( } } - // Copy assets - if (!options.watch && options.assets?.length) { - try { - await copyAssets( - normalizeAssetPatterns( - options.assets, - new virtualFs.SyncDelegateHost(host), - root, - normalize(projectRoot), - projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot), - ), - Array.from(outputPaths.values()), - context.workspaceRoot, - ); - } catch (err) { - return { success: false, error: 'Unable to copy assets: ' + err.message }; + // Check for budget errors and display them to the user. + const budgets = options.budgets; + if (budgets?.length) { + const budgetFailures = checkBudgets(budgets, webpackStats, processResults); + for (const { severity, message } of budgetFailures) { + switch (severity) { + case ThresholdSeverity.Warning: + webpackStats.warnings.push(message); + break; + case ThresholdSeverity.Error: + webpackStats.errors.push(message); + break; + default: + assertNever(severity); + } } } - for (const [locale, outputPath] of outputPaths.entries()) { - let localeBaseHref; - if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') { - localeBaseHref = urlJoin( - options.baseHref || '', - i18n.locales[locale].baseHref ?? `/${locale}/`, - ); + const buildSuccess = success && !statsHasErrors(webpackStats); + if (buildSuccess) { + // Copy assets + if (!options.watch && options.assets?.length) { + spinner.start('Copying assets...'); + try { + await copyAssets( + normalizeAssetPatterns( + options.assets, + root, + normalize(projectRoot), + projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot), + ), + Array.from(outputPaths.values()), + context.workspaceRoot, + ); + spinner.succeed('Copying assets complete.'); + } catch (err) { + spinner.fail(colors.redBright('Copying of assets failed.')); + + return { success: false, error: 'Unable to copy assets: ' + err.message }; + } } - try { - if (options.index) { - await writeIndexHtml({ - host, - outputPath: path.join(outputPath, getIndexOutputFile(options)), - indexPath: path.join(context.workspaceRoot, getIndexInputFile(options)), - files, - noModuleFiles, - moduleFiles, - baseHref: localeBaseHref || options.baseHref, - deployUrl: options.deployUrl, - sri: options.subresourceIntegrity, - scripts: options.scripts, - styles: options.styles, - postTransforms: indexTransforms, - crossOrigin: options.crossOrigin, - // i18nLocale is used when Ivy is disabled - lang: locale || options.i18nLocale, - }); + if (options.index) { + spinner.start('Generating index html...'); + + const WOFFSupportNeeded = !buildBrowserFeatures.isFeatureSupported('woff2'); + const entrypoints = generateEntryPoints({ + scripts: options.scripts ?? [], + styles: options.styles ?? [], + }); + + const indexHtmlGenerator = new IndexHtmlGenerator({ + indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)), + entrypoints, + deployUrl: options.deployUrl, + sri: options.subresourceIntegrity, + WOFFSupportNeeded, + optimization: normalizedOptimization, + crossOrigin: options.crossOrigin, + postTransform: transforms.indexHtml, + }); + + for (const [locale, outputPath] of outputPaths.entries()) { + try { + const { content, warnings, errors } = await indexHtmlGenerator.process({ + baseHref: getLocaleBaseHref(i18n, locale) || options.baseHref, + // i18nLocale is used when Ivy is disabled + lang: locale || options.i18nLocale, + outputPath, + files: mapEmittedFilesToFileInfo(files), + noModuleFiles: mapEmittedFilesToFileInfo(noModuleFiles), + moduleFiles: mapEmittedFilesToFileInfo(moduleFiles), + }); + + if (warnings.length || errors.length) { + spinner.stop(); + warnings.forEach(m => context.logger.warn(m)); + errors.forEach(m => context.logger.error(m)); + spinner.start(); + } + + const indexOutput = path.join(outputPath, getIndexOutputFile(options.index)); + await mkdir(path.dirname(indexOutput), { recursive: true }); + await writeFile(indexOutput, content); + } catch (error) { + spinner.fail('Index html generation failed.'); + + return { success: false, error: mapErrorToMessage(error) }; + } } - if (options.serviceWorker) { - await augmentAppWithServiceWorker( - host, - root, - normalize(projectRoot), - normalize(outputPath), - localeBaseHref || options.baseHref || '/', - options.ngswConfigPath, - ); + spinner.succeed('Index html generation complete.'); + } + + if (options.serviceWorker) { + spinner.start('Generating service worker...'); + for (const [locale, outputPath] of outputPaths.entries()) { + try { + await augmentAppWithServiceWorker( + root, + normalize(projectRoot), + normalize(outputPath), + getLocaleBaseHref(i18n, locale) || options.baseHref || '/', + options.ngswConfigPath, + ); + } catch (error) { + spinner.fail('Service worker generation failed.'); + + return { success: false, error: mapErrorToMessage(error) }; + } } - } catch (err) { - return { success: false, error: mapErrorToMessage(err) }; + + spinner.succeed('Service worker generation complete.'); } } - } - return { success }; + webpackStatsLogger(context.logger, webpackStats, config, bundleInfoStats); + + return { success: buildSuccess }; + } }), map( event => @@ -791,8 +751,19 @@ export function buildWebpackBrowser( } as BrowserBuilderOutput), ), ); - }), - ); + }), + ); + + function getLocaleBaseHref(i18n: I18nOptions, locale: string): string | undefined { + if (i18n.locales[locale] && i18n.locales[locale]?.baseHref !== '') { + return urlJoin( + options.baseHref || '', + i18n.locales[locale].baseHref ?? `/${locale}/`, + ); + } + + return undefined; + } } function mapErrorToMessage(error: unknown): string | undefined { @@ -808,8 +779,37 @@ function mapErrorToMessage(error: unknown): string | undefined { } function assertNever(input: never): never { - throw new Error(`Unexpected call to assertNever() with input: ${ - JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); + throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`); +} + +type ArrayElement = A extends ReadonlyArray ? T : never; +function generateBundleInfoStats( + bundle: ProcessBundleFile, + chunk: ArrayElement | undefined, + chunkType: ChunkType, +): BundleStats { + return generateBundleStats( + { + size: bundle.size, + files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename], + names: chunk?.names, + entry: !!chunk?.names.includes('runtime'), + initial: !!chunk?.initial, + rendered: true, + chunkType, + }, + ); +} + +function mapEmittedFilesToFileInfo(files: EmittedFiles[] = []): FileInfo[] { + const filteredFiles: FileInfo[] = []; + for (const { file, name, extension, initial } of files) { + if (name && initial) { + filteredFiles.push({ file, extension, name }); + } + } + + return filteredFiles; } export default createBuilder(buildWebpackBrowser); diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 628b5dcd8dd8..ef16e332d1a3 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -57,7 +57,7 @@ "additionalProperties": false }, "optimization": { - "description": "Enables optimization of the build output.", + "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.", "x-user-analytics": 16, "default": false, "oneOf": [ @@ -70,12 +70,32 @@ "default": true }, "styles": { - "type": "boolean", "description": "Enables optimization of the styles output.", - "default": true + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "minify": { + "type": "boolean", + "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.", + "default": true + }, + "inlineCritical": { + "type": "boolean", + "description": "Extract and inline critical CSS definitions to improve first paint time.", + "default": false + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] }, "fonts": { - "description": "Enables optimization for fonts. This requires internet access.", + "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", "default": true, "oneOf": [ { @@ -83,7 +103,7 @@ "properties": { "inline": { "type": "boolean", - "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.", + "description": "Reduce render blocking requests by inlining external Google fonts and icons CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.", "default": true } }, @@ -126,7 +146,7 @@ "default": false }, "sourceMap": { - "description": "Output sourcemaps.", + "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.", "default": true, "oneOf": [ { @@ -134,22 +154,22 @@ "properties": { "scripts": { "type": "boolean", - "description": "Output sourcemaps for all scripts.", + "description": "Output source maps for all scripts.", "default": true }, "styles": { "type": "boolean", - "description": "Output sourcemaps for all styles.", + "description": "Output source maps for all styles.", "default": true }, "hidden": { "type": "boolean", - "description": "Output sourcemaps used for error reporting tools.", + "description": "Output source maps used for error reporting tools.", "default": false }, "vendor": { "type": "boolean", - "description": "Resolve vendor packages sourcemaps.", + "description": "Resolve vendor packages source maps.", "default": false } }, @@ -162,12 +182,12 @@ }, "vendorChunk": { "type": "boolean", - "description": "Use a separate bundle containing only vendor libraries.", + "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.", "default": true }, "commonChunk": { "type": "boolean", - "description": "Use a separate bundle containing code used across multiple bundles.", + "description": "Generate a seperate bundle containing code used across multiple bundles.", "default": true }, "baseHref": { @@ -185,7 +205,8 @@ }, "progress": { "type": "boolean", - "description": "Log progress to the console while building." + "description": "Log progress to the console while building.", + "default": true }, "i18nFile": { "type": "string", @@ -209,6 +230,7 @@ "default": "warning" }, "localize": { + "description": "Translate the bundles in one or more locales.", "oneOf": [ { "type": "boolean", @@ -348,12 +370,6 @@ }, "default": [] }, - "rebaseRootRelativeCssUrls": { - "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", - "type": "boolean", - "default": false, - "x-deprecated": true - }, "webWorkerTsConfig": { "type": "string", "description": "TypeScript configuration for Web Worker modules." @@ -395,6 +411,11 @@ { "type": "object", "properties": { + "followSymlinks": { + "type": "boolean", + "default": false, + "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched." + }, "glob": { "type": "string", "description": "The pattern to match." @@ -433,10 +454,12 @@ "type": "object", "properties": { "src": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "replaceWith": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -449,10 +472,12 @@ "type": "object", "properties": { "replace": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" }, "with": { - "type": "string" + "type": "string", + "pattern": "\\.(([cm]?j|t)sx?|json)$" } }, "additionalProperties": false, @@ -474,6 +499,7 @@ }, "bundleName": { "type": "string", + "pattern": "^[\\w\\-.]*$", "description": "The bundle name for this extra entry point." }, "inject": { diff --git a/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts index 0952c61b3f0a..e118ea340896 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts index 51e855b10f71..5f6ab2a32012 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts index 1e2483bd4ec6..2ec91d854c14 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts index 5f1db812ab0d..7dcf3d1ac438 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts index 58ff90668a6e..f2935305cd2e 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts index e5c88b01f68e..c80d88910ff1 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts index 3eaed7bb7d63..3016a7881c88 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -99,7 +99,6 @@ describe('Browser Builder bundle budgets', () => { const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); const output = await run.result; expect(output.success).toBe(true); - expect(logs.length).toBe(2); expect(logs.join()).toMatch(`Warning.+app\.component\.${ext}`); await run.stop(); }); @@ -139,7 +138,6 @@ describe('Browser Builder bundle budgets', () => { const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); const output = await run.result; expect(output.success).toBe(false); - expect(logs.length).toBe(2); expect(logs.join()).toMatch(`Error.+app\.component\.${ext}`); await run.stop(); }); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts index ecd710e25841..474203bb5361 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts deleted file mode 100644 index 9d469531a7c6..000000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Architect } from '@angular-devkit/architect'; -import { logging } from '@angular-devkit/core'; -import { createArchitect, host } from '../../test-utils'; - -describe('Browser Builder commonjs warning', () => { - const targetSpec = { project: 'app', target: 'build' }; - - let architect: Architect; - let logger: logging.Logger; - let logs: string[]; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - - // Create logger - logger = new logging.Logger(''); - logs = []; - logger.subscribe(e => logs.push(e.message)); - }); - - afterEach(async () => host.restore().toPromise()); - - for (const aot of [true, false]) { - it(`should not show warning for styles import in ${aot ? 'AOT' : 'JIT'} Mode`, async () => { - // Add a Common JS dependency - host.appendToFile('src/app/app.component.ts', ` - import '../../test.css'; - `); - - host.writeMultipleFiles({ - './test.css': ` - body { - color: red; - }; - `, - }); - - const run = await architect.scheduleTarget(targetSpec, { aot }, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - expect(logs.join()).not.toContain('Warning'); - await run.stop(); - }); - - it(`should show warning when depending on a Common JS bundle in ${aot ? 'AOT' : 'JIT'} Mode`, async () => { - // Add a Common JS dependency - host.appendToFile('src/app/app.component.ts', ` - import 'bootstrap'; - `); - - const run = await architect.scheduleTarget(targetSpec, { aot }, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - const logMsg = logs.join(); - expect(logMsg).toMatch(/Warning: .+app\.component\.ts depends on 'bootstrap'\. CommonJS or AMD dependencies/); - expect(logMsg).not.toContain('jquery', 'Should not warn on transitive CommonJS packages which parent is also CommonJS.'); - await run.stop(); - }); - } - - it('should not show warning when depending on a Common JS bundle which is allowed', async () => { - // Add a Common JS dependency - host.appendToFile('src/app/app.component.ts', ` - import 'bootstrap'; - import 'zone.js/dist/zone-error'; - `); - - const overrides = { - allowedCommonJsDependencies: [ - 'bootstrap', - 'zone.js', - ], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - expect(logs.join()).not.toContain('Warning'); - await run.stop(); - }); - - it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => { - // Add a Common JS dependency - host.appendToFile('src/app/app.component.ts', ` - import '@angular/common/locales/fr'; - `); - - const run = await architect.scheduleTarget(targetSpec, undefined, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - - expect(logs.join()).not.toContain('Warning'); - await run.stop(); - }); - - it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => { - host.replaceInFile('tsconfig.json', /"baseUrl": ".\/",/, ` - "baseUrl": "./", - "paths": { - "@app/*": [ - "src/app/*" - ] - }, - `); - - host.replaceInFile('src/app/app.module.ts', './app.component', '@app/app.component'); - - const run = await architect.scheduleTarget(targetSpec, { aot: false }, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - - expect(logs.join()).not.toContain('WARNING'); - await run.stop(); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts index 65f4684caf48..bf83f20cfb18 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts index 11205efaca1b..6392bc351317 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts index 293c8d3a11b7..f8039bfe57e7 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -50,7 +50,9 @@ describe('Browser Builder with differential loading', () => { 'runtime-es5.js.map', 'vendor-es2015.js', + 'vendor-es2015.js.map', 'vendor-es5.js', + 'vendor-es5.js.map', 'styles.css', 'styles.css.map', @@ -88,7 +90,9 @@ describe('Browser Builder with differential loading', () => { 'runtime-es5.js.map', 'vendor-es2016.js', + 'vendor-es2016.js.map', 'vendor-es5.js', + 'vendor-es5.js.map', 'styles.css', 'styles.css.map', @@ -126,7 +130,9 @@ describe('Browser Builder with differential loading', () => { 'runtime-es5.js.map', 'vendor-esnext.js', + 'vendor-esnext.js.map', 'vendor-es5.js', + 'vendor-es5.js.map', 'styles.css', 'styles.css.map', @@ -142,16 +148,17 @@ describe('Browser Builder with differential loading', () => { 'favicon.ico', 'index.html', - 'main.js', - 'main.js.map', + 'main-es2015.js', + 'main-es2015.js.map', - 'polyfills.js', - 'polyfills.js.map', + 'polyfills-es2015.js', + 'polyfills-es2015.js.map', - 'runtime.js', - 'runtime.js.map', + 'runtime-es2015.js', + 'runtime-es2015.js.map', - 'vendor.js', + 'vendor-es2015.js', + 'vendor-es2015.js.map', 'styles.css', 'styles.css.map', @@ -195,10 +202,10 @@ describe('Browser Builder with differential loading', () => { const { files } = await browserBuild(architect, host, target, { watch: true }); expect(await files['index.html']).toContain( - '' + - '' + - '' + - '', + '' + + '' + + '' + + '', ); }); }); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts index 8d9a44a36b41..67dd8d71276d 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts index 8075059db024..e448cca3a8bd 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts index 669a3bc6384c..12f6203726f1 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts index f74d864b4ab2..b8a5ecd61d49 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts new file mode 100644 index 000000000000..7782b288d61d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 } from '@angular-devkit/architect'; +import { browserBuild, createArchitect, host } from '../../test-utils'; + +describe('Browser Builder inline critical CSS optimization', () => { + const target = { project: 'app', target: 'build' }; + const overrides = { + optimization: { + scripts: false, + styles: { + minify: true, + inlineCritical: true, + }, + fonts: false, + }, + }; + + let architect: Architect; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + host.writeMultipleFiles({ + 'src/styles.css': ` + body { color: #000 } + `, + }); + }); + + afterEach(async () => host.restore().toPromise()); + + it('works', async () => { + const { files } = await browserBuild(architect, host, target, overrides); + const html = await files['index.html']; + expect(html).toContain(``); + expect(html).toContain(`body{color:#000}`); + }); + + it('works with deployUrl', async () => { + const { files } = await browserBuild(architect, host, target, { ...overrides, deployUrl: 'http://cdn.com/' }); + const html = await files['index.html']; + expect(html).toContain(``); + expect(html).toContain(`body{color:#000}`); + }); + + it('should not inline critical css when option is disabled', async () => { + const { files } = await browserBuild(architect, host, target, { optimization: false }); + const html = await files['index.html']; + expect(html).toContain(``); + expect(html).not.toContain(`body{color:#000}`); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts index ffb07abf2026..b00f7796e1c4 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -9,7 +9,7 @@ import { Architect } from '@angular-devkit/architect'; import { TestProjectHost } from '@angular-devkit/architect/testing'; import { logging } from '@angular-devkit/core'; -import { take, tap, timeout } from 'rxjs/operators'; +import { debounceTime, take, tap } from 'rxjs/operators'; import { browserBuild, createArchitect, @@ -117,7 +117,7 @@ describe('Browser Builder lazy modules', () => { const run = await architect.scheduleTarget(target, overrides); await run.output .pipe( - timeout(15000), + debounceTime(3000), tap(buildEvent => { buildNumber++; switch (buildNumber) { diff --git a/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts deleted file mode 100644 index 056d6449a6fd..000000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Architect } from '@angular-devkit/architect'; -import { browserBuild, createArchitect, host } from '../../test-utils'; - - -describe('Browser Builder license extraction', () => { - const target = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - afterEach(async () => host.restore().toPromise()); - - // Ignored because license works when trying manually on a project, but doesn't work here. - it('works', async () => { - // TODO: make license extraction independent from optimization level. - const overrides = { extractLicenses: true, optimization: true }; - - const { files } = await browserBuild(architect, host, target, overrides); - expect(await files['3rdpartylicenses.txt']).not.toBeUndefined(); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts index 705a035eb49b..d9c8dfd17a59 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts index f5025f2d9dcb..7fe49602081f 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts deleted file mode 100644 index a557117f7614..000000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { - browserBuild, - createArchitect, - host, - lazyModuleFiles, - lazyModuleFnImport, -} from '../../test-utils'; - -describe('Browser Builder output hashing', () => { - const target = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - afterEach(async () => host.restore().toPromise()); - - it('updates hash as content changes', async () => { - const OUTPUT_RE = /(main|styles|lazy\.module)\.([a-z0-9]+)\.(chunk|bundle)\.(js|css)$/; - - function generateFileHashMap(): Map { - const hashes = new Map(); - - host - .scopedSync() - .list(normalize('./dist')) - .forEach(name => { - const matches = name.match(OUTPUT_RE); - if (matches) { - const [, module, hash] = matches; - hashes.set(module, hash); - } - }); - - return hashes; - } - - function validateHashes( - oldHashes: Map, - newHashes: Map, - shouldChange: Array, - ): void { - newHashes.forEach((hash, module) => { - if (hash == oldHashes.get(module)) { - if (shouldChange.includes(module)) { - throw new Error( - `Module "${module}" did not change hash (${hash}), but was expected to.`, - ); - } - } else if (!shouldChange.includes(module)) { - throw new Error(`Module "${module}" changed hash (${hash}), but was not expected to.`); - } - }); - } - - let oldHashes: Map; - let newHashes: Map; - - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - const overrides = { outputHashing: 'all', extractCss: true }; - - // We must do several builds instead of a single one in watch mode, so that the output - // path is deleted on each run and only contains the most recent files. - await browserBuild(architect, host, target, overrides); - - // Save the current hashes. - oldHashes = generateFileHashMap(); - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, []); - oldHashes = newHashes; - host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' }); - - // Style hash should change. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['styles']); - oldHashes = newHashes; - host.writeMultipleFiles({ 'src/app/app.component.css': 'h1 { margin: 10px; }' }); - - // Main hash should change, since inline styles go in the main bundle. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['main']); - oldHashes = newHashes; - host.appendToFile('src/app/lazy/lazy.module.ts', `console.log(1);`); - - // Lazy loaded bundle should change, and so should inline. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['lazy.module']); - oldHashes = newHashes; - host.appendToFile('src/main.ts', ''); - - // Nothing should have changed. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, []); - }); - - it('supports options', async () => { - host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` }); - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - // We must do several builds instead of a single one in watch mode, so that the output - // path is deleted on each run and only contains the most recent files. - // 'all' should hash everything. - await browserBuild(architect, host, target, { outputHashing: 'all', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy(); - - // 'none' should hash nothing. - await browserBuild(architect, host, target, { outputHashing: 'none', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy(); - - // 'media' should hash css resources only. - await browserBuild(architect, host, target, { outputHashing: 'media', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy(); - - // 'bundles' should hash bundles only. - await browserBuild(architect, host, target, { outputHashing: 'bundles', extractCss: true }); - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy(); - }); - - it('does not hash non injected styles', async () => { - const overrides = { - outputHashing: 'all', - extractCss: true, - styles: [{ input: 'src/styles.css', inject: false }], - }; - - await browserBuild(architect, host, target, overrides); - - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy(); - expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true); - }); - - it('does not hash non injected styles when optimization is enabled', async () => { - const overrides = { - outputHashing: 'all', - extractCss: true, - optimization: true, - styles: [{ input: 'src/styles.css', inject: false }], - }; - - await browserBuild(architect, host, target, overrides); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy(); - expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts index beb600d77adc..e27ae3fc63b2 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts index 9f330fc759f5..8deb9abbee41 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts index 9162cd0cda6b..10f209f5483b 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts index 795cbadaf918..b0adf56849fd 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts index 373ac3419984..4eb458472e36 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts index fb9e0a87b6ae..b0c027bb2427 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts index 127e406cea24..d460055302e8 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts index 29e004bf8428..a2569242ac6b 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -81,20 +81,20 @@ describe('Browser Builder scripts array', () => { 'lazy-script.js': 'lazy-script', 'renamed-script.js': 'pre-rename-script', 'renamed-lazy-script.js': 'pre-rename-lazy-script', - 'main.js': 'input-script', - 'index.html': '' - + '' + 'main-es2015.js': 'input-script', + 'index.html': '' + + '' + '' + '' - + '' - + '', + + '' + + '', }; host.writeMultipleFiles(scripts); host.appendToFile('src/main.ts', '\nimport \'./input-script.js\';'); // Enable differential loading - host.appendToFile('.browserslistrc', '\nIE 10'); + host.appendToFile('.browserslistrc', '\nIE 11'); // Remove styles so we don't have to account for them in the index.html order check. const { files } = await browserBuild(architect, host, target, { diff --git a/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts index 7ce6b377b740..15ca52b5d050 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts index b7ea20ef94a9..5e8981d9ac09 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -158,4 +158,20 @@ describe('Browser Builder source map', () => { expect(await files['styles.css']).not.toContain('sourceMappingURL=styles.css.map'); expect(await files['styles.css']).not.toContain('sourceMappingURL=data:application/json'); }); + + it('should resolve sources to partial SCSS files', async () => { + const overrides = { + sourceMap: true, + extractCss: true, + styles: ['src/styles.scss'], + }; + + host.writeMultipleFiles({ + 'src/styles.scss': `@import './partial';`, + 'src/_partial.scss': `p { color: red; }`, + }); + + const { files } = await browserBuild(architect, host, target, overrides); + expect(await files['styles.css.map']).toContain('_partial.scss'); + }); }); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts index 4ef7c3a5c0b6..116828fe4bbe 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts index 7da64970b00e..176b3b241df3 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -354,7 +354,7 @@ describe('Browser Builder styles', () => { }); // TODO: consider making this a unit test in the url processing plugins. - it(`supports baseHref/deployUrl in resource urls without rebaseRootRelativeCssUrls`, async () => { + it(`supports baseHref/deployUrl in resource urls`, async () => { // Use a large image for the relative ref so it cannot be inlined. host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png'); host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png'); @@ -458,118 +458,6 @@ describe('Browser Builder styles', () => { expect(main).toContain(`url('/assets/component-img-absolute.svg')`); }, 90000); - it(`supports baseHref/deployUrl in resource urls with rebaseRootRelativeCssUrls`, async () => { - // Use a large image for the relative ref so it cannot be inlined. - host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png'); - host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png'); - host.writeMultipleFiles({ - 'src/styles.css': ` - h1 { background: url('/assets/global-img-absolute.svg'); } - h2 { background: url('./assets/global-img-relative.png'); } - `, - 'src/app/app.component.css': ` - h3 { background: url('/assets/component-img-absolute.svg'); } - h4 { background: url('../assets/component-img-relative.png'); } - `, - 'src/assets/global-img-absolute.svg': imgSvg, - 'src/assets/component-img-absolute.svg': imgSvg, - }); - - // Check base paths are correctly generated. - const overrides = { - extractCss: true, - rebaseRootRelativeCssUrls: true, - }; - let { files } = await browserBuild(architect, host, target, { - ...overrides, - aot: true, - }); - - let styles = await files['styles.css']; - let main = await files['main.js']; - expect(styles).toContain(`url('/assets/global-img-absolute.svg')`); - expect(styles).toContain(`url('global-img-relative.png')`); - expect(main).toContain(`url('/assets/component-img-absolute.svg')`); - expect(main).toContain(`url('component-img-relative.png')`); - expect(host.scopedSync().exists(normalize('dist/assets/global-img-absolute.svg'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/global-img-relative.png'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/assets/component-img-absolute.svg'))).toBe( - true, - ); - expect(host.scopedSync().exists(normalize('dist/component-img-relative.png'))).toBe(true); - - // Check urls with deploy-url scheme are used as is. - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: '/base/', - deployUrl: 'http://deploy.url/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('http://deploy.url/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('http://deploy.url/assets/component-img-absolute.svg')`); - - // Check urls with base-href scheme are used as is (with deploy-url). - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: 'http://base.url/', - deployUrl: 'deploy/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('http://base.url/deploy/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('http://base.url/deploy/assets/component-img-absolute.svg')`); - - // Check urls with deploy-url and base-href scheme only use deploy-url. - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: 'http://base.url/', - deployUrl: 'http://deploy.url/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('http://deploy.url/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('http://deploy.url/assets/component-img-absolute.svg')`); - - // Check with schemeless base-href and deploy-url flags. - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: '/base/', - deployUrl: 'deploy/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('/base/deploy/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('/base/deploy/assets/component-img-absolute.svg')`); - - // Check with identical base-href and deploy-url flags. - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: '/base/', - deployUrl: '/base/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('/base/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('/base/assets/component-img-absolute.svg')`); - - // Check with only base-href flag. - files = (await browserBuild(architect, host, target, { - ...overrides, - baseHref: '/base/', - })).files; - - styles = await files['styles.css']; - main = await files['main.js']; - expect(styles).toContain(`url('/base/assets/global-img-absolute.svg')`); - expect(main).toContain(`url('/base/assets/component-img-absolute.svg')`); - }, 90000); - it(`supports bootstrap@4 with full path`, async () => { const bootstrapPath = dirname(require.resolve('bootstrap/package.json')); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts deleted file mode 100644 index 274d89889a71..000000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Architect } from '@angular-devkit/architect'; -import { browserBuild, createArchitect, host } from '../../test-utils'; - - -describe('Browser Builder subresource integrity', () => { - const target = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - afterEach(async () => host.restore().toPromise()); - - it('works', async () => { - host.writeMultipleFiles({ - 'src/my-js-file.js': `console.log(1); export const a = 2;`, - 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`, - }); - - const overrides = { subresourceIntegrity: true }; - const { files } = await browserBuild(architect, host, target, overrides); - expect(await files['index.html']).toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts index 3e32677c00ff..d416c7c0a2da 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts index 8e31ff766798..21eeaf44ea06 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts index e8b920ab1fdc..7f62ebc53e81 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts index e78237b25186..6fe4f211fd55 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts index 11b8fc0dc9ed..31e2124ec985 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license @@ -34,7 +34,7 @@ describe('Browser Builder external source map', () => { expect(hasTsSourcePaths).toBe(true, `vendor.js.map should have '.ts' extentions`); }); - it(`does not generate 'vendor.js.map' when vendor sourcemap is disabled`, async () => { + it('does not map sourcemaps from external library when disabled', async () => { const overrides = { sourceMap: { scripts: true, @@ -44,6 +44,8 @@ describe('Browser Builder external source map', () => { }; const { files } = await browserBuild(architect, host, target, overrides); - expect(files['vendor.js.map']).toBeUndefined(); + const sourcePaths: string[] = JSON.parse(await files['vendor.js.map']).sources; + const hasTsSourcePaths = sourcePaths.some(p => path.extname(p) == '.ts'); + expect(hasTsSourcePaths).toBe(false, `vendor.js.map not should have '.ts' extentions`); }); }); diff --git a/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts index 9d8392c430ed..a9897e965889 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts index 75b2eda17272..50bbce771bef 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts @@ -1,37 +1,45 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import { describeBuilder } from '../../testing'; +import { buildWebpackBrowser } from '../index'; -import { Architect } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { browserBuild, createArchitect, host } from '../../test-utils'; +const BROWSER_BUILDER_INFO = { + name: '@angular-devkit/build-angular:browser', + schemaPath: __dirname + '/../schema.json', +}; +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('basic test', () => { + it('works', async () => { + // Provide a target and options for builder execution + harness.useTarget('build', { + outputPath: 'dist', + index: 'src/index.html', + main: 'src/main.ts', + polyfills: 'src/polyfills.ts', + tsConfig: 'src/tsconfig.app.json', + progress: false, + assets: ['src/favicon.ico', 'src/assets'], + styles: ['src/styles.css'], + scripts: [], + }); -describe('Browser Builder basic test', () => { - const target = { project: 'app', target: 'build' }; - let architect: Architect; + // Execute builder with above provided project, target, and options + await harness.executeOnce(); - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(async () => host.restore().toPromise()); - - it('works', async () => { - await browserBuild(architect, host, target); - - // Default files should be in outputPath. - expect(await host.scopedSync().exists(normalize('dist/runtime.js'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/main.js'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/polyfills.js'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/vendor.js'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/favicon.ico'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true); - expect(await host.scopedSync().exists(normalize('dist/index.html'))).toBe(true); + // Default files should be in outputPath. + expect(harness.hasFile('dist/runtime.js')).toBe(true); + expect(harness.hasFile('dist/main.js')).toBe(true); + expect(harness.hasFile('dist/polyfills.js')).toBe(true); + expect(harness.hasFile('dist/vendor.js')).toBe(true); + expect(harness.hasFile('dist/favicon.ico')).toBe(true); + expect(harness.hasFile('dist/styles.css')).toBe(true); + expect(harness.hasFile('dist/index.html')).toBe(true); + }); }); }); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts new file mode 100644 index 000000000000..3cc82124298f --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable: no-big-function +import { logging } from '@angular-devkit/core'; +import { concatMap, count, take, timeout } from 'rxjs/operators'; +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuild Error"', () => { + + it('detects template errors with no AOT codegen or TS emit differences', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + aot: true, + watch: true, + }); + + const goodDirectiveContents = ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir' }) + export class Dir { + @Input() foo: number; + } + `; + + const typeErrorText = `Type 'number' is not assignable to type 'string'.`; + + // Create a directive and add to application + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + await harness.writeFile('src/app/app.module.ts', ` + import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { Dir } from './dir'; + @NgModule({ + declarations: [ + AppComponent, + Dir, + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `); + + // Create app component that uses the directive + await harness.writeFile('src/app/app.component.ts', ` + import { Component } from '@angular/core' + @Component({ + selector: 'app-root', + template: '

', + }) + export class AppComponent { } + `); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(60000), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + + // Update directive to use a different input type for 'foo' (number -> string) + // Should cause a template error + await harness.writeFile('src/app/dir.ts', ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir' }) + export class Dir { + @Input() foo: string; + } + `); + + break; + case 1: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 2: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir.ts', goodDirectiveContents); + + break; + case 3: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 4: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + break; + } + }), + take(5), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(5); + }); + + it('detects template errors with AOT codegen differences', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + aot: true, + watch: true, + }); + + const typeErrorText = `Type 'number' is not assignable to type 'string'.`; + + // Create two directives and add to application + await harness.writeFile('src/app/dir.ts', ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir' }) + export class Dir { + @Input() foo: number; + } + `); + + // Same selector with a different type on the `foo` property but initially no `@Input` + const goodDirectiveContents = ` + import { Directive } from '@angular/core'; + @Directive({ selector: 'dir' }) + export class Dir2 { + foo: string; + } + `; + await harness.writeFile('src/app/dir2.ts', goodDirectiveContents); + + await harness.writeFile('src/app/app.module.ts', ` + import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { AppComponent } from './app.component'; + import { Dir } from './dir'; + import { Dir2 } from './dir2'; + @NgModule({ + declarations: [ + AppComponent, + Dir, + Dir2, + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `); + + // Create app component that uses the directive + await harness.writeFile('src/app/app.component.ts', ` + import { Component } from '@angular/core' + @Component({ + selector: 'app-root', + template: '', + }) + export class AppComponent { } + `); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(60000), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + + // Update second directive to use string property `foo` as an Input + // Should cause a template error + await harness.writeFile('src/app/dir2.ts', ` + import { Directive, Input } from '@angular/core'; + @Directive({ selector: 'dir' }) + export class Dir2 { + @Input() foo: string; + } + `); + + break; + case 1: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should persist error in the next rebuild + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 2: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Revert the directive change that caused the error + // Should remove the error + await harness.writeFile('src/app/dir2.ts', goodDirectiveContents); + + break; + case 3: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + // Make an unrelated change to verify error cache was updated + // Should continue showing no error + await harness.modifyFile('src/main.ts', (content) => content + '\n'); + + break; + case 4: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(typeErrorText), + }), + ); + + break; + } + }), + take(5), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(5); + }); + + it('recovers from component stylesheet error', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(30000), + concatMap(async ({ result, logs }, index) => { + switch (index) { + case 0: + expect(result?.success).toBeTrue(); + await harness.writeFile('src/app/app.component.css', 'invalid-css-content'); + + break; + case 1: + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + await harness.writeFile('src/app/app.component.css', 'p { color: green }'); + + break; + case 2: + expect(result?.success).toBeTrue(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('invalid-css-content'), + }), + ); + + harness.expectFile('dist/main.js').content.toContain('p { color: green }'); + + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts new file mode 100644 index 000000000000..b9c7ba230e08 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Behavior: "TypeScript Configuration - target"', () => { + it('downlevels async functions when targetting ES2017', async () => { + // Set TypeScript configuration target to ES2017 to enable native async + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es2017'; + + return JSON.stringify(tsconfig); + }); + + // Add a JavaScript file with async code + await harness.writeFile( + 'src/async-test.js', + 'async function testJs() { console.log("from-async-js-function"); }', + ); + + // Add an async function to the project as well as JavaScript file + await harness.modifyFile( + 'src/main.ts', + (content) => + 'import "./async-test";\n' + + content + + `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'), + }), + ); + + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js').content.toContain('"from-async-app-function"'); + harness.expectFile('dist/main.js').content.toContain('"from-async-js-function"'); + }); + + it('creates correct sourcemaps when downleveling async functions', async () => { + // Set TypeScript configuration target to ES2017 to enable native async + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es2017'; + + return JSON.stringify(tsconfig); + }); + + // Add a JavaScript file with async code + await harness.writeFile( + 'src/async-test.js', + 'async function testJs() { console.log("from-async-js-function"); }', + ); + + // Add an async function to the project as well as JavaScript file + // The type `Void123` is used as a unique identifier for the final sourcemap + // If sourcemaps are not properly propagated then it will not be in the final sourcemap + await harness.modifyFile( + 'src/main.ts', + (content) => + 'import "./async-test";\n' + + content + + '\ntype Void123 = void;' + + `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + sourceMap: { + scripts: true, + }, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js.map').content.toContain('Promise'); + }); + + it('downlevels async functions when targetting greater than ES2017', async () => { + // Set TypeScript configuration target greater than ES2017 to enable native async + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsconfig = JSON.parse(content); + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + tsconfig.compilerOptions.target = 'es2020'; + + return JSON.stringify(tsconfig); + }); + + // Add an async function to the project + await harness.writeFile( + 'src/main.ts', + 'async function test(): Promise { console.log("from-async-function"); }', + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'), + }), + ); + + harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/); + harness.expectFile('dist/main.js').content.toContain('"from-async-function"'); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts new file mode 100644 index 000000000000..8da71c1b3315 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging } from '@angular-devkit/core'; +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, harness => { + describe('Option: "allowedCommonJsDependencies"', () => { + describe('given option is not set', () => { + for (const aot of [true, false]) { + it(`should not show warning for styles import in ${aot ? 'AOT' : 'JIT'} Mode`, async () => { + await harness.writeFile('./test.css', `body { color: red; };`); + await harness.appendToFile('src/app/app.component.ts', `import '../../test.css';`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + aot, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it(`should show warning when depending on a Common JS bundle in ${aot ? 'AOT' : 'JIT'} Mode`, async () => { + // Add a Common JS dependency + await harness.appendToFile('src/app/app.component.ts', `import 'bootstrap';`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + aot, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Warning: .+app\.component\.ts depends on 'bootstrap'\. CommonJS or AMD dependencies/), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('jquery') }), + 'Should not warn on transitive CommonJS packages which parent is also CommonJS.', + ); + }); + } + }); + + it('should not show warning when depending on a Common JS bundle which is allowed', async () => { + // Add a Common JS dependency + await harness.appendToFile('src/app/app.component.ts', ` + import 'bootstrap'; + import 'zone.js/dist/zone-error'; + `); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [ + 'bootstrap', + 'zone.js', + ], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => { + await harness.appendToFile('src/app/app.component.ts', `import '@angular/common/locales/fr';`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + + it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => { + await harness.modifyFile( + 'tsconfig.json', content => { + return content.replace(/"baseUrl": ".\/",/, ` + "baseUrl": "./", + "paths": { + "@app/*": [ + "src/app/*" + ] + }, + `); + }); + + await harness.modifyFile( + 'src/app/app.module.ts', + content => content.replace('./app.component', '@app/app.component'), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + allowedCommonJsDependencies: [], + aot: false, + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/CommonJS or AMD dependencies/), + }), + ); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts new file mode 100644 index 000000000000..cf9a6a97a9c1 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts @@ -0,0 +1,394 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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-big-function +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "assets"', () => { + beforeEach(async () => { + // Application code is not needed for asset tests + await harness.writeFile('src/main.ts', ''); + }); + + it('supports an empty array value', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + it('supports mixing shorthand and longhand syntax', async () => { + await harness.writeFile('src/files/test.svg', ''); + await harness.writeFile('src/files/another.file', 'asset file'); + await harness.writeFile('src/extra.file', 'extra file'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['src/extra.file', { glob: '*', input: 'src/files', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.file').content.toBe('extra file'); + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + }); + + describe('shorthand syntax', () => { + it('copies a single asset', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['src/test.svg'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + }); + + it('copies multiple assets', async () => { + await harness.writeFile('src/test.svg', ''); + await harness.writeFile('src/another.file', 'asset file'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['src/test.svg', 'src/another.file'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + }); + + it('copies an asset with directory and maintains directory in output', async () => { + await harness.writeFile('src/subdirectory/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['src/subdirectory/test.svg'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/subdirectory/test.svg').content.toBe(''); + }); + + it('does not fail if asset does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['src/test.svg'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').toNotExist(); + }); + + it('throws exception if asset path is not within project source root', async () => { + await harness.writeFile('test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: ['test.svg'], + }); + + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); + + expect(result).toBeUndefined(); + expect(error).toEqual( + jasmine.objectContaining({ + message: jasmine.stringMatching('path must start with the project source root'), + }), + ); + + harness.expectFile('dist/test.svg').toNotExist(); + }); + }); + + describe('longhand syntax', () => { + it('copies a single asset', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + }); + + it('copies multiple assets as separate entries', async () => { + await harness.writeFile('src/test.svg', ''); + await harness.writeFile('src/another.file', 'asset file'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { glob: 'test.svg', input: 'src', output: '.' }, + { glob: 'another.file', input: 'src', output: '.' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + }); + + it('copies multiple assets with a single entry glob pattern', async () => { + await harness.writeFile('src/test.svg', ''); + await harness.writeFile('src/another.file', 'asset file'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: '{test.svg,another.file}', input: 'src', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + }); + + it('copies multiple assets with a wildcard glob pattern', async () => { + await harness.writeFile('src/files/test.svg', ''); + await harness.writeFile('src/files/another.file', 'asset file'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: '*', input: 'src/files', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + }); + + it('copies multiple assets with a recursive wildcard glob pattern', async () => { + await harness.writeFiles({ + 'src/files/test.svg': '', + 'src/files/another.file': 'asset file', + 'src/files/nested/extra.file': 'extra file', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: '**/*', input: 'src/files', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').content.toBe('asset file'); + harness.expectFile('dist/nested/extra.file').content.toBe('extra file'); + }); + + it('automatically ignores "." prefixed files when using wildcard glob pattern', async () => { + await harness.writeFile('src/files/.gitkeep', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: '*', input: 'src/files', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/.gitkeep').toNotExist(); + }); + + it('supports ignoring a specific file when using a glob pattern', async () => { + await harness.writeFiles({ + 'src/files/test.svg': '', + 'src/files/another.file': 'asset file', + 'src/files/nested/extra.file': 'extra file', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { glob: '**/*', input: 'src/files', output: '.', ignore: ['another.file'] }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').toNotExist(); + harness.expectFile('dist/nested/extra.file').content.toBe('extra file'); + }); + + it('supports ignoring with a glob pattern when using a glob pattern', async () => { + await harness.writeFiles({ + 'src/files/test.svg': '', + 'src/files/another.file': 'asset file', + 'src/files/nested/extra.file': 'extra file', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [ + { glob: '**/*', input: 'src/files', output: '.', ignore: ['**/*.file'] }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + harness.expectFile('dist/another.file').toNotExist(); + harness.expectFile('dist/nested/extra.file').toNotExist(); + }); + + it('copies an asset with directory and maintains directory in output', async () => { + await harness.writeFile('src/subdirectory/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'subdirectory/test.svg', input: 'src', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/subdirectory/test.svg').content.toBe(''); + }); + + it('does not fail if asset does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').toNotExist(); + }); + + it('uses project output path when output option is empty string', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + }); + + it('uses project output path when output option is "."', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '.' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + }); + + it('uses project output path when output option is "/"', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '/' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').content.toBe(''); + }); + + it('creates a project output sub-path when output option path does not exist', async () => { + await harness.writeFile('src/test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: 'subdirectory' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/subdirectory/test.svg').content.toBe(''); + }); + + it('throws exception if output option is not within project output path', async () => { + await harness.writeFile('test.svg', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + assets: [{ glob: 'test.svg', input: 'src', output: '..' }], + }); + + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); + + expect(result).toBeUndefined(); + expect(error).toEqual( + jasmine.objectContaining({ + message: jasmine.stringMatching( + 'An asset cannot be written to a location outside of the output path', + ), + }), + ); + + harness.expectFile('dist/test.svg').toNotExist(); + }); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts new file mode 100644 index 000000000000..5a978435e76b --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "extractLicenses"', () => { + it(`should generate '3rdpartylicenses.txt' when 'extractLicenses' is true`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('MIT'); + }); + + it(`should not generate '3rdpartylicenses.txt' when 'extractLicenses' is false`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: false, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/3rdpartylicenses.txt').toNotExist(); + }); + + it(`should not generate '3rdpartylicenses.txt' when 'extractLicenses' is not set`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + harness.expectFile('dist/3rdpartylicenses.txt').toNotExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts new file mode 100644 index 000000000000..62d8d64ab3fc --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "main"', () => { + it('uses a provided TypeScript file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + main: 'src/main.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/runtime.js').toExist(); + harness.expectFile('dist/main.js').toExist(); + harness.expectFile('dist/vendor.js').toExist(); + harness.expectFile('dist/index.html').toExist(); + }); + + it('uses a provided JavaScript file', async () => { + await harness.writeFile('src/main.js', `console.log('main');`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + main: 'src/main.js', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/runtime.js').toExist(); + harness.expectFile('dist/main.js').toExist(); + harness.expectFile('dist/vendor.js').toNotExist(); + harness.expectFile('dist/index.html').toExist(); + + harness.expectFile('dist/main.js').content.toContain(`console.log('main')`); + }); + + it('fails and shows an error when file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + main: 'src/missing.ts', + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }), + ); + + harness.expectFile('dist/runtime.js').toNotExist(); + harness.expectFile('dist/main.js').toNotExist(); + harness.expectFile('dist/vendor.js').toNotExist(); + harness.expectFile('dist/index.html').toNotExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts new file mode 100644 index 000000000000..14db3ee47213 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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 { buildWebpackBrowser } from '../../index'; +import { OutputHashing } from '../../schema'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "outputHashing"', () => { + beforeEach(async () => { + // Application code is not needed for asset tests + await harness.writeFile('src/main.ts', ''); + }); + + it('hashes all filenames when set to "all"', async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.All, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue(); + }); + + it(`doesn't hash any filenames when not set`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it(`doesn't hash any filenames when set to "none"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.None, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it(`hashes CSS resources filenames only when set to "media"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.Media, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue(); + }); + + it(`hashes bundles filenames only when set to "bundles"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.Bundles, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it('does not hash non injected styles', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.All, + styles: [{ + input: 'src/styles.css', + inject: false, + }], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js.map$/)).toBeFalse(); + harness.expectFile('dist/styles.css').toExist(); + harness.expectFile('dist/styles.css.map').toExist(); + }); + + it('does not override different files which has the same filenames when hashing is "none"', async () => { + await harness.writeFiles({ + 'src/styles.css': ` + h1 { background: url('./test.svg')} + h2 { background: url('./small/test.svg')} + `, + './src/test.svg': ` + Hello World + `, + './src/small/test.svg': ` + Hello World + `, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.None, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').toExist(); + harness.expectFile('dist/small-test.svg').toExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts new file mode 100644 index 000000000000..587fe85be84c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "polyfills"', () => { + it('uses a provided TypeScript file', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/polyfills.ts', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/polyfills.js').toExist(); + }); + + it('uses a provided JavaScript file', async () => { + await harness.writeFile('src/polyfills.js', `console.log('main');`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/polyfills.js', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/polyfills.js').content.toContain(`console.log('main')`); + }); + + it('fails and shows an error when file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + polyfills: 'src/missing.ts', + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }), + ); + + harness.expectFile('dist/polyfills.js').toNotExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts new file mode 100644 index 000000000000..abe0b4cc1b47 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts @@ -0,0 +1,416 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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-big-function +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "scripts"', () => { + beforeEach(async () => { + // Application code is not needed for scripts tests + await harness.writeFile('src/main.ts', ''); + }); + + it('supports an empty array value', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + }); + + describe('shorthand syntax', () => { + it('processes a single script into a single output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: ['src/test-script-a.js'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/scripts.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple scripts into a single output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + await harness.writeFile('src/test-script-b.js', 'console.log("b");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: ['src/test-script-a.js', 'src/test-script-b.js'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/scripts.js').content.toContain('console.log("a")'); + harness.expectFile('dist/scripts.js').content.toContain('console.log("b")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('preserves order of multiple scripts in single output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + await harness.writeFile('src/test-script-b.js', 'console.log("b");'); + await harness.writeFile('src/test-script-c.js', 'console.log("c");'); + await harness.writeFile('src/test-script-d.js', 'console.log("d");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [ + 'src/test-script-c.js', + 'src/test-script-d.js', + 'src/test-script-b.js', + 'src/test-script-a.js', + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/scripts.js') + .content.toMatch( + /console\.log\("c"\)[;\s]+console\.log\("d"\)[;\s]+console\.log\("b"\)[;\s]+console\.log\("a"\)/, + ); + }); + + it('throws an exception if script does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: ['src/test-script-a.js'], + }); + + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); + + expect(result).toBeUndefined(); + expect(error).toEqual( + jasmine.objectContaining({ + message: jasmine.stringMatching(`Script file src/test-script-a.js does not exist.`), + }), + ); + + harness.expectFile('dist/scripts.js').toNotExist(); + }); + + it('shows the output script as a chunk entry in the logging output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: ['src/test-script-a.js'], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }), + ); + }); + }); + + describe('longhand syntax', () => { + it('processes a single script into a single output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/scripts.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes a single script into a single output named with bundleName', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('uses default bundleName when bundleName is empty string', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js', bundleName: '' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/scripts.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple scripts with no bundleName into a single output', async () => { + await harness.writeFiles({ + 'src/test-script-a.js': 'console.log("a");', + 'src/test-script-b.js': 'console.log("b");', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js' }, { input: 'src/test-script-b.js' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/scripts.js').content.toContain('console.log("a")'); + harness.expectFile('dist/scripts.js').content.toContain('console.log("b")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple scripts with same bundleName into a single output', async () => { + await harness.writeFiles({ + 'src/test-script-a.js': 'console.log("a");', + 'src/test-script-b.js': 'console.log("b");', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [ + { input: 'src/test-script-a.js', bundleName: 'extra' }, + { input: 'src/test-script-b.js', bundleName: 'extra' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.js').content.toContain('console.log("a")'); + harness.expectFile('dist/extra.js').content.toContain('console.log("b")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple scripts with different bundleNames into separate outputs', async () => { + await harness.writeFiles({ + 'src/test-script-a.js': 'console.log("a");', + 'src/test-script-b.js': 'console.log("b");', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [ + { input: 'src/test-script-a.js', bundleName: 'extra' }, + { input: 'src/test-script-b.js', bundleName: 'other' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.js').content.toContain('console.log("a")'); + harness.expectFile('dist/other.js').content.toContain('console.log("b")'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('preserves order of multiple scripts in single output', async () => { + await harness.writeFiles({ + 'src/test-script-a.js': 'console.log("a");', + 'src/test-script-b.js': 'console.log("b");', + 'src/test-script-c.js': 'console.log("c");', + 'src/test-script-d.js': 'console.log("d");', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [ + { input: 'src/test-script-c.js' }, + { input: 'src/test-script-d.js' }, + { input: 'src/test-script-b.js' }, + { input: 'src/test-script-a.js' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/scripts.js') + .content.toMatch( + /console\.log\("c"\)[;\s]+console\.log\("d"\)[;\s]+console\.log\("b"\)[;\s]+console\.log\("a"\)/, + ); + }); + + it('preserves order of multiple scripts with different bundleNames', async () => { + await harness.writeFiles({ + 'src/test-script-a.js': 'console.log("a");', + 'src/test-script-b.js': 'console.log("b");', + 'src/test-script-c.js': 'console.log("c");', + 'src/test-script-d.js': 'console.log("d");', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [ + { input: 'src/test-script-c.js', bundleName: 'other' }, + { input: 'src/test-script-d.js', bundleName: 'extra' }, + { input: 'src/test-script-b.js', bundleName: 'extra' }, + { input: 'src/test-script-a.js', bundleName: 'other' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/other.js') + .content.toMatch(/console\.log\("c"\)[;\s]+console\.log\("a"\)/); + harness + .expectFile('dist/extra.js') + .content.toMatch(/console\.log\("d"\)[;\s]+console\.log\("b"\)/); + harness + .expectFile('dist/index.html') + .content.toMatch( + /'); + }); + + it('does not add script element to index when inject is false', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js', inject: false }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + // `inject: false` causes the bundleName to be the input file name + harness.expectFile('dist/test-script-a.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.not.toContain(''); + }); + + it('does not add script element to index with bundleName when inject is false', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra', inject: false }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.js').content.toContain('console.log("a")'); + harness + .expectFile('dist/index.html') + .content.not.toContain(''); + }); + + it('shows the output script as a chunk entry in the logging output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js' }], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }), + ); + }); + + it('shows the output script as a chunk entry with bundleName in the logging output', async () => { + await harness.writeFile('src/test-script-a.js', 'console.log("a");'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra' }], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.js.+\d+ bytes/) }), + ); + }); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts new file mode 100644 index 000000000000..177799a51171 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "statsJson"', () => { + beforeEach(async () => { + // Application code is not needed for stat JSON tests + await harness.writeFile('src/main.ts', ''); + }); + + it('generates a Webpack Stats file in output when true', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + statsJson: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + if (harness.expectFile('dist/stats.json').toExist()) { + const content = harness.readFile('dist/stats.json'); + expect(() => JSON.parse(content)) + .withContext('Expected Webpack Stats file to be valid JSON.') + .not.toThrow(); + } + }); + + it('includes Webpack profiling information', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + statsJson: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + if (harness.expectFile('dist/stats.json').toExist()) { + const stats = JSON.parse(harness.readFile('dist/stats.json')); + expect(stats?.chunks?.[0]?.modules?.[0]?.profile?.building).toBeDefined(); + } + }); + + it('does not generate a Webpack Stats file in output when false', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + statsJson: false, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/stats.json').toNotExist(); + }); + + it('does not generate a Webpack Stats file in output when not present', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/stats.json').toNotExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts new file mode 100644 index 000000000000..e726fbaf565a --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts @@ -0,0 +1,432 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use 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-big-function +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "styles"', () => { + beforeEach(async () => { + // Application code is not needed for styles tests + await harness.writeFile('src/main.ts', ''); + }); + + it('supports an empty array value', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').toNotExist(); + }); + + it('does not create an output styles file when option is not present', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').toNotExist(); + }); + + describe('shorthand syntax', () => { + it('processes a single style into a single output', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/test-style-a.css'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple styles into a single output', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/test-style-a.css', 'src/test-style-b.css'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness.expectFile('dist/styles.css').content.toContain('.test-b {color: green}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('preserves order of multiple styles in single output', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + 'src/test-style-c.css': '.test-c {color: blue}', + 'src/test-style-d.css': '.test-d {color: yellow}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [ + 'src/test-style-c.css', + 'src/test-style-d.css', + 'src/test-style-b.css', + 'src/test-style-a.css', + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/styles.css') + .content.toMatch( + /\.test-c {color: blue}\s+\.test-d {color: yellow}\s+\.test-b {color: green}\s+\.test-a {color: red}/, + ); + }); + + it('fails and shows an error if style does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/test-style-a.css'], + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }), + ); + + harness.expectFile('dist/styles.css').toNotExist(); + }); + + it('shows the output style as a chunk entry in the logging output', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/test-style-a.css'], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }), + ); + }); + }); + + describe('longhand syntax', () => { + it('processes a single style into a single output', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes a single style into a single output named with bundleName', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', bundleName: 'extra' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('uses default bundleName when bundleName is empty string', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', bundleName: '' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple styles with no bundleName into a single output', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css' }, { input: 'src/test-style-b.css' }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness.expectFile('dist/styles.css').content.toContain('.test-b {color: green}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple styles with same bundleName into a single output', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [ + { input: 'src/test-style-a.css', bundleName: 'extra' }, + { input: 'src/test-style-b.css', bundleName: 'extra' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}'); + harness.expectFile('dist/extra.css').content.toContain('.test-b {color: green}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('processes multiple styles with different bundleNames into separate outputs', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [ + { input: 'src/test-style-a.css', bundleName: 'extra' }, + { input: 'src/test-style-b.css', bundleName: 'other' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}'); + harness.expectFile('dist/other.css').content.toContain('.test-b {color: green}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('preserves order of multiple styles in single output', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + 'src/test-style-c.css': '.test-c {color: blue}', + 'src/test-style-d.css': '.test-d {color: yellow}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [ + { input: 'src/test-style-c.css' }, + { input: 'src/test-style-d.css' }, + { input: 'src/test-style-b.css' }, + { input: 'src/test-style-a.css' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/styles.css') + .content.toMatch( + /\.test-c {color: blue}\s+\.test-d {color: yellow}\s+\.test-b {color: green}\s+\.test-a {color: red}/, + ); + }); + + it('preserves order of multiple styles with different bundleNames', async () => { + await harness.writeFiles({ + 'src/test-style-a.css': '.test-a {color: red}', + 'src/test-style-b.css': '.test-b {color: green}', + 'src/test-style-c.css': '.test-c {color: blue}', + 'src/test-style-d.css': '.test-d {color: yellow}', + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [ + { input: 'src/test-style-c.css', bundleName: 'other' }, + { input: 'src/test-style-d.css', bundleName: 'extra' }, + { input: 'src/test-style-b.css', bundleName: 'extra' }, + { input: 'src/test-style-a.css', bundleName: 'other' }, + ], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness + .expectFile('dist/other.css') + .content.toMatch(/\.test-c {color: blue}\s+\.test-a {color: red}/); + harness + .expectFile('dist/extra.css') + .content.toMatch(/\.test-d {color: yellow}\s+\.test-b {color: green}/); + harness + .expectFile('dist/index.html') + .content.toMatch( + /\s*/, + ); + }); + + it('adds link element to index when inject is true', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', inject: true }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.toContain(''); + }); + + it('does not add link element to index when inject is false', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', inject: false }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + // `inject: false` causes the bundleName to be the input file name + harness.expectFile('dist/test-style-a.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.not.toContain(''); + }); + + it('does not add link element to index with bundleName when inject is false', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', bundleName: 'extra', inject: false }], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}'); + harness + .expectFile('dist/index.html') + .content.not.toContain(''); + }); + + it('shows the output style as a chunk entry in the logging output', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css' }], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }), + ); + }); + + it('shows the output style as a chunk entry with bundleName in the logging output', async () => { + await harness.writeFile('src/test-style-a.css', '.test-a {color: red}'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: [{ input: 'src/test-style-a.css', bundleName: 'extra' }], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + expect(logs).toContain( + jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.css.+\d+ bytes/) }), + ); + }); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts new file mode 100644 index 000000000000..3561aa2c332a --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable:no-big-function +import { logging } from '@angular-devkit/core'; +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "subresourceIntegrity"', () => { + it(`does not add integrity attribute when not present`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/index.html').content.not.toContain('integrity='); + }); + + it(`does not add integrity attribute when 'false'`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + subresourceIntegrity: false, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/index.html').content.not.toContain('integrity='); + }); + + it(`does add integrity attribute when 'true'`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + subresourceIntegrity: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/index.html').content.toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/); + }); + + it(`does not issue a warning when 'true' and 'scripts' is set.`, async () => { + await harness.writeFile('src/script.js', ''); + + harness.useTarget('build', { + ...BASE_OPTIONS, + subresourceIntegrity: true, + scripts: ['src/script.js'], + }); + + const { result, logs } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/index.html').content.toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(/subresource-integrity/), + }), + ); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts new file mode 100644 index 000000000000..289c5f780a01 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "tsConfig"', () => { + it('uses a provided TypeScript configuration file', async () => { + // Setup a TS file that uses ES2015+ const and then target ES5. + // The const usage should be downleveled in the output if the TS config is used. + await harness.writeFile('src/main.ts', 'const a = 5; console.log(a);'); + await harness.writeFile( + 'src/tsconfig.option.json', + JSON.stringify({ + compilerOptions: { + target: 'es5', + types: [], + }, + files: ['main.ts'], + }), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + tsConfig: 'src/tsconfig.option.json', + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + + harness.expectFile('dist/main.js').content.not.toContain('const'); + }); + + it('throws an exception when TypeScript Configuration file does not exist', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + tsConfig: 'src/missing.json', + }); + + const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); + + expect(result).toBeUndefined(); + expect(error).toEqual( + jasmine.objectContaining({ + message: jasmine.stringMatching('no such file or directory'), + }), + ); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts new file mode 100644 index 000000000000..6b4bc73fb92d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { concatMap, count, take, timeout } from 'rxjs/operators'; +import { buildWebpackBrowser } from '../../index'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "watch"', () => { + it('does not wait for file changes when false', (done) => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: false, + }); + + // If the build waits then it will timeout with the custom timeout. + // A single build should not take more than 15 seconds. + let count = 0; + harness + .execute() + .pipe(timeout(15000)) + .subscribe({ + complete() { + expect(count).toBe(1); + done(); + }, + next({ result }) { + count++; + expect(result?.success).toBe(true); + }, + error(error) { + done.fail(error); + }, + }); + }); + + it('does not wait for file changes when not present', (done) => { + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + // If the build waits then it will timeout with the custom timeout. + // A single build should not take more than 15 seconds. + let count = 0; + harness + .execute() + .pipe(timeout(15000)) + .subscribe({ + complete() { + expect(count).toBe(1); + done(); + }, + next({ result }) { + count++; + expect(result?.success).toBe(true); + }, + error(error) { + done.fail(error); + }, + }); + }); + + it('watches for file changes when true', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + main: 'src/main.ts', + watch: true, + }); + + const buildCount = await harness + .execute() + .pipe( + timeout(30000), + concatMap(async ({ result }, index) => { + expect(result?.success).toBe(true); + + switch (index) { + case 0: + harness.expectFile('dist/main.js').content.not.toContain('abcd1234'); + + await harness.modifyFile( + 'src/main.ts', + (content) => content + 'console.log("abcd1234");', + ); + break; + case 1: + harness.expectFile('dist/main.js').content.toContain('abcd1234'); + break; + } + }), + take(2), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(2); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/setup.ts b/packages/angular_devkit/build_angular/src/browser/tests/setup.ts new file mode 100644 index 000000000000..d511bd2ac8ad --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/setup.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Schema } from '../schema'; + +export { describeBuilder } from '../../testing'; + +export const BROWSER_BUILDER_INFO = Object.freeze({ + name: '@angular-devkit/build-angular:browser', + schemaPath: __dirname + '/../schema.json', +}); + +/** + * Contains all required browser builder fields. + * Also disables progress reporting to minimize logging output. + */ +export const BASE_OPTIONS = Object.freeze({ + index: 'src/index.html', + main: 'src/main.ts', + outputPath: 'dist', + tsConfig: 'src/tsconfig.app.json', + progress: false, +}); diff --git a/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts index c36d18182906..27b659128094 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts index 735db97a68d6..f40df3e3ab26 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts new file mode 100644 index 000000000000..56cf7e7d8471 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Architect } from '@angular-devkit/architect'; +import { logging } from '@angular-devkit/core'; +import { createArchitect, host } from '../test-utils'; + +describe('Dev Server Builder commonjs warning', () => { + const targetSpec = { project: 'app', target: 'serve' }; + + let architect: Architect; + let logger: logging.Logger; + let logs: string[]; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + + // Create logger + logger = new logging.Logger(''); + logs = []; + logger.subscribe(e => logs.push(e.message)); + }); + + afterEach(async () => host.restore().toPromise()); + + it('should not show warning when using HMR', async () => { + const run = await architect.scheduleTarget(targetSpec, { hmr: true }, { logger }); + const output = await run.result; + expect(output.success).toBe(true); + expect(logs.join()).not.toContain('Warning'); + await run.stop(); + }); + + it('should not show warning when using live-reload', async () => { + const run = await architect.scheduleTarget(targetSpec, { liveReload: true}, { logger }); + const output = await run.result; + expect(output.success).toBe(true); + expect(logs.join()).not.toContain('Warning'); + await run.stop(); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts index 125b8cd05ae3..757d0ed1527a 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license diff --git a/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts index a4215a6b180c..a617ccd5a45b 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts @@ -1,15 +1,15 @@ /** * @license - * Copyright Google Inc. All Rights Reserved. + * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Architect, BuilderRun } from '@angular-devkit/architect'; // tslint:disable: no-implicit-dependencies -import puppeteer from 'puppeteer/lib/cjs/puppeteer'; import { Browser } from 'puppeteer/lib/cjs/puppeteer/common/Browser'; import { Page } from 'puppeteer/lib/cjs/puppeteer/common/Page'; +import puppeteer from 'puppeteer/lib/cjs/puppeteer/node'; // tslint:enable: no-implicit-dependencies import { debounceTime, switchMap, take } from 'rxjs/operators'; import { createArchitect, host } from '../test-utils'; @@ -56,8 +56,8 @@ describe('Dev Server Builder HMR', () => { 'src/app/app.component.html': `

{{title}}

- - + +