diff --git a/.appveyor.yml b/.appveyor.yml index bc5a2ad092..5e490c30dd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -9,11 +9,15 @@ install: - ps: Install-Product node $env:nodejs_version - npm install -g npm@~5.6.0 - npm install + # Appveyor (via chocolatey) cannot use older versions of Chrome: + # https://github.com/chocolatey/chocolatey-coreteampackages/tree/master/automatic/googlechrome + - npm run webdriver-update-appveyor test_script: - node --version - npm --version - - npm run test + - npm test + - npm run test-large build: off diff --git a/.circleci/bazel.rc b/.circleci/bazel.rc new file mode 100644 index 0000000000..0cf9444d5d --- /dev/null +++ b/.circleci/bazel.rc @@ -0,0 +1,20 @@ +# These options are enabled when running on CI +# We do this by copying this file to /etc/bazel.bazelrc at the start of the build. + +# Echo all the configuration settings and their source +build --announce_rc + +# Don't be spammy in the logs +build --noshow_progress + +# Don't run manual tests +test --test_tag_filters=-manual + +# Workaround https://github.com/bazelbuild/bazel/issues/3645 +# Bazel doesn't calculate the memory ceiling correctly when running under Docker. +# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default: +# https://circleci.com/docs/2.0/configuration-reference/#resource_class +build --local_resources=3072,2.0,1.0 + +# Retry in the event of flakes +test --flaky_test_attempts=2 diff --git a/.circleci/config.yml b/.circleci/config.yml index 921234b8c7..235770cd46 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,39 +8,50 @@ _defaults: &defaults _post_checkout: &post_checkout post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge" +_root_package_lock_key: &_root_package_lock_key + key: angular_devkit-{{ checksum "package-lock.json" }} + jobs: - lint: + install: <<: *defaults steps: - - checkout: - <<: *post_checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: npm install --no-save + - save_cache: + <<: *_root_package_lock_key + paths: + - "node_modules" + + lint: + <<: *defaults + steps: + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: npm run lint validate: <<: *defaults steps: - - checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - - - run: npm install --no-save + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: npm run validate -- --ci test: <<: *defaults steps: - - checkout: - <<: *post_checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - - - run: npm install --no-save + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: npm run test -- --code-coverage --full + test-large: + <<: *defaults + steps: + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key + - run: npm run webdriver-update-circleci + - run: npm run test-large -- --code-coverage --full + integration: <<: *defaults steps: @@ -53,7 +64,6 @@ jobs: - restore_cache: key: angular_devkit-integration-aio-{{ checksum "tests/@angular_devkit/build_optimizer/webpack/aio-app/package-lock.json" }} - - run: npm install --no-save - run: xvfb-run -a npm run integration - save_cache: @@ -68,29 +78,23 @@ jobs: build: <<: *defaults steps: - - checkout: - <<: *post_checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - - - run: bazel run @nodejs//:npm install - - run: bazel build //packages/... + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: npm run admin -- build - - save_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - paths: - - "node_modules" + # build-bazel: + # <<: *defaults + # steps: + # - checkout: *post_checkout + # - run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc + # - run: bazel run @nodejs//:npm install + # - run: bazel build //packages/... snapshot_publish: <<: *defaults steps: - - checkout: - <<: *post_checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - - - run: npm install --no-save + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: name: Decrypt Credentials command: | @@ -103,12 +107,8 @@ jobs: publish: <<: *defaults steps: - - checkout: - <<: *post_checkout - - restore_cache: - key: angular_devkit-{{ checksum "package-lock.json" }} - - - run: npm install --no-save + - checkout: *post_checkout + - restore_cache: *_root_package_lock_key - run: name: Decrypt Credentials command: | @@ -122,15 +122,27 @@ workflows: version: 2 default_workflow: jobs: - - lint - - validate + - install + - lint: + requires: + - install + - validate: + requires: + - install - build: requires: - lint - validate + # - build-bazel: + # requires: + # - lint + # - validate - test: requires: - build + - test-large: + requires: + - build - integration: requires: - build diff --git a/.monorepo.json b/.monorepo.json index 831fdf62b9..0bf4759b15 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -42,8 +42,19 @@ }, "packages": { "@_/benchmark": { - "version": "0.4.2", - "hash": "b1f9f2d8e1eca2a865d39263f0ca5a38" + "version": "0.5.6", + "hash": "37ed7e417435c92dbf301e7b26ea37ca" + }, + "devkit": { + "version": "0.5.6", + "hash": "0f2c1274c1a33354e21586921fd4a29a" + }, + "@angular/pwa": { + "name": "Angular PWA Schematics", + "section": "Schematics", + "version": "0.5.6", + "hash": "4cf615db59e59d114672e927f35c8973", + "snapshotRepo": "angular/angular-pwa-builds" }, "@angular-devkit/architect": { "name": "Architect", @@ -53,14 +64,14 @@ "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/architect/README.md" } ], - "version": "0.0.3", - "hash": "57e25727cb63a879effb10fd30599987", + "version": "0.5.6", + "hash": "67cca04a43ff8a1bc5f3c12423569752", "snapshotRepo": "angular/angular-devkit-architect-builds" }, "@angular-devkit/architect-cli": { "name": "Architect CLI", - "version": "0.0.3", - "hash": "c270aceb888087c61c0647771045fd47", + "version": "0.5.6", + "hash": "437cdb100a812bc10a08131cde7c3028", "snapshotRepo": "angular/angular-devkit-architect-cli-builds" }, "@angular-devkit/build-optimizer": { @@ -71,21 +82,33 @@ "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_optimizer/README.md" } ], - "version": "0.4.2", - "hash": "55d5c82b05ef9b515e143356a75b895e", + "version": "0.5.6", + "hash": "31b9b564ff5e540ae704d8723445d1ad", "snapshotRepo": "angular/angular-devkit-build-optimizer-builds" }, - "@angular-devkit/build-webpack": { - "name": "Build Webpack", + "@angular-devkit/build-ng-packagr": { + "name": "Build NgPackagr", + "links": [ + { + "label": "README", + "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_ng_packagr/README.md" + } + ], + "version": "0.5.6", + "hash": "ef46e636df1a32ffd43ca795dbe20607", + "snapshotRepo": "angular/angular-devkit-build-ng-packagr-builds" + }, + "@angular-devkit/build-angular": { + "name": "Build Angular", "links": [ { "label": "README", - "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_webpack/README.md" + "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_angular/README.md" } ], - "version": "0.0.3", - "hash": "8165f0fc4dce547561e62e4e7605ea6e", - "snapshotRepo": "angular/angular-devkit-build-webpack-builds" + "version": "0.5.6", + "hash": "0de6ff5665a03357e496a40ebab64d84", + "snapshotRepo": "angular/angular-devkit-build-angular-builds" }, "@angular-devkit/core": { "name": "Core", @@ -95,8 +118,8 @@ "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/core/README.md" } ], - "version": "0.4.2", - "hash": "3dc30c470d7cc9f3c7f226b4259eec28", + "version": "0.5.6", + "hash": "89c39fd109e35cfc39ca2d45f6da29ee", "snapshotRepo": "angular/angular-devkit-core-builds" }, "@angular-devkit/schematics": { @@ -107,35 +130,50 @@ "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/schematics/README.md" } ], - "version": "0.4.2", - "hash": "9ac6f3bb734fa1e1751a005fb3c3124a", + "version": "0.5.6", + "hash": "5b1953260312043a7ad13929aaf62e3f", "snapshotRepo": "angular/angular-devkit-schematics-builds" }, "@angular-devkit/schematics-cli": { "name": "Schematics CLI", - "version": "0.4.2", - "hash": "9df0b747019a04f8e01caddd642e7b52", + "version": "0.5.6", + "hash": "8e8ed56f8334efecbbcdaa889fb5e5fd", "snapshotRepo": "angular/angular-devkit-schematics-cli-builds" }, + "@ngtools/webpack": { + "name": "Webpack Angular Plugin", + "version": "6.0.0-rc.4", + "section": "Misc", + "hash": "4346a38d1e70711671d3a5cac2959663", + "snapshotRepo": "angular/ngtools-webpack-builds" + }, "@schematics/angular": { "name": "Angular Schematics", "section": "Schematics", - "version": "0.4.2", - "hash": "f2f1253db8e7a01eb0e5a945dd08979c", + "version": "0.5.6", + "hash": "8a0904c4dacef4ff1e27f4641adfe187", "snapshotRepo": "angular/schematics-angular-builds" }, "@schematics/schematics": { "name": "Schematics Schematics", - "version": "0.4.2", + "version": "0.5.6", "section": "Schematics", - "hash": "c9723671e270d9d6eb7f4cdbfc37c77e" + "hash": "81ea3d90a8e85b15fd83c847af72c26c", + "snapshotRepo": "angular/schematics-schematics-builds" }, "@schematics/package-update": { "name": "Package JSON Update Schematics", - "version": "0.4.2", + "version": "0.5.6", "section": "Schematics", - "hash": "1abc8090ac37083ca02f92ab794c6d7f", + "hash": "53654c259405585a652ba71c93cf7269", "snapshotRepo": "angular/schematics-package-update-builds" + }, + "@schematics/update": { + "name": "Package Update Schematics", + "version": "0.5.6", + "section": "Schematics", + "hash": "ac963f112efee0a83293d6b7d24bca89", + "snapshotRepo": "angular/schematics-update-builds" } } } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a6f40ca5e5..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -dist: trusty - -language: node_js - -env: - global: - - DBUS_SESSION_BUS_ADDRESS=/dev/null - -matrix: - fast_finish: true - include: - - node_js: "8" - os: linux - env: SCRIPT=lint - - node_js: "8" - os: linux - env: SCRIPT=test - -before_install: - # Install yarn. - - curl -o- -L https://yarnpkg.com/install.sh | bash - - export PATH="$HOME/.yarn/bin:$PATH" - - yarn config set spin false - - yarn config set progress false - -script: - - if [[ "$SCRIPT" ]]; then npm run $SCRIPT; fi diff --git a/README.md b/README.md index 75905a71f0..a96f3614bb 100644 --- a/README.md +++ b/README.md @@ -50,20 +50,29 @@ This is a monorepo which contains many packages: | Project | Package | Version | Links | |---|---|---|---| -**Architect** | [`@angular-devkit/architect`](http://npmjs.com/packages/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](http://npmjs.com/packages/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/architect/README.md) -**Architect CLI** | [`@angular-devkit/architect-cli`](http://npmjs.com/packages/@angular-devkit/architect-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect-cli/latest.svg)](http://npmjs.com/packages/@angular-devkit/architect-cli) | -**Build Optimizer** | [`@angular-devkit/build-optimizer`](http://npmjs.com/packages/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](http://npmjs.com/packages/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_optimizer/README.md) -**Build Webpack** | [`@angular-devkit/build-webpack`](http://npmjs.com/packages/@angular-devkit/build-webpack) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-webpack/latest.svg)](http://npmjs.com/packages/@angular-devkit/build-webpack) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_webpack/README.md) -**Core** | [`@angular-devkit/core`](http://npmjs.com/packages/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](http://npmjs.com/packages/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/core/README.md) -**Schematics** | [`@angular-devkit/schematics`](http://npmjs.com/packages/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](http://npmjs.com/packages/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/schematics/README.md) -**Schematics CLI** | [`@angular-devkit/schematics-cli`](http://npmjs.com/packages/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](http://npmjs.com/packages/@angular-devkit/schematics-cli) | +**Architect** | [`@angular-devkit/architect`](https://npmjs.com/package/@angular-devkit/architect) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect/latest.svg)](https://npmjs.com/package/@angular-devkit/architect) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/architect/README.md) +**Architect CLI** | [`@angular-devkit/architect-cli`](https://npmjs.com/package/@angular-devkit/architect-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Farchitect-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/architect-cli) | +**Build Angular** | [`@angular-devkit/build-angular`](https://npmjs.com/package/@angular-devkit/build-angular) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-angular/latest.svg)](https://npmjs.com/package/@angular-devkit/build-angular) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_angular/README.md) +**Build NgPackagr** | [`@angular-devkit/build-ng-packagr`](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-ng-packagr/latest.svg)](https://npmjs.com/package/@angular-devkit/build-ng-packagr) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_ng_packagr/README.md) +**Build Optimizer** | [`@angular-devkit/build-optimizer`](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](https://npmjs.com/package/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_optimizer/README.md) +**Core** | [`@angular-devkit/core`](https://npmjs.com/package/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](https://npmjs.com/package/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/core/README.md) +**Schematics** | [`@angular-devkit/schematics`](https://npmjs.com/package/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/schematics/README.md) +**Schematics CLI** | [`@angular-devkit/schematics-cli`](https://npmjs.com/package/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](https://npmjs.com/package/@angular-devkit/schematics-cli) | #### Schematics | Project | Package | Version | Links | |---|---|---|---| -**Angular Schematics** | [`@schematics/angular`](http://npmjs.com/packages/@schematics/angular) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fangular/latest.svg)](http://npmjs.com/packages/@schematics/angular) | -**Package JSON Update Schematics** | [`@schematics/package-update`](http://npmjs.com/packages/@schematics/package-update) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fpackage-update/latest.svg)](http://npmjs.com/packages/@schematics/package-update) | -**Schematics Schematics** | [`@schematics/schematics`](http://npmjs.com/packages/@schematics/schematics) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fschematics/latest.svg)](http://npmjs.com/packages/@schematics/schematics) | +**Angular PWA Schematics** | [`@angular/pwa`](https://npmjs.com/package/@angular/pwa) | [![latest](https://img.shields.io/npm/v/%40angular%2Fpwa/latest.svg)](https://npmjs.com/package/@angular/pwa) | +**Angular Schematics** | [`@schematics/angular`](https://npmjs.com/package/@schematics/angular) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fangular/latest.svg)](https://npmjs.com/package/@schematics/angular) | +**Package JSON Update Schematics** | [`@schematics/package-update`](https://npmjs.com/package/@schematics/package-update) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fpackage-update/latest.svg)](https://npmjs.com/package/@schematics/package-update) | +**Schematics Schematics** | [`@schematics/schematics`](https://npmjs.com/package/@schematics/schematics) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fschematics/latest.svg)](https://npmjs.com/package/@schematics/schematics) | +**Package Update Schematics** | [`@schematics/update`](https://npmjs.com/package/@schematics/update) | [![latest](https://img.shields.io/npm/v/%40schematics%2Fupdate/latest.svg)](https://npmjs.com/package/@schematics/update) | + +#### Misc + +| Project | Package | Version | Links | +|---|---|---|---| +**Webpack Angular Plugin** | [`@ngtools/webpack`](https://npmjs.com/package/@ngtools/webpack) | [![latest](https://img.shields.io/npm/v/%40ngtools%2Fwebpack/latest.svg)](https://npmjs.com/package/@ngtools/webpack) | diff --git a/bin/architect b/bin/architect old mode 100644 new mode 100755 diff --git a/bin/devkit-admin b/bin/devkit-admin index 5a2f0460cb..477aa4ae82 100755 --- a/bin/devkit-admin +++ b/bin/devkit-admin @@ -50,13 +50,6 @@ try { output.write(color(entry.message) + '\n'); }); - - logger - .pipe(filter(entry => entry.level === 'fatal')) - .subscribe(() => { - process.stderr.write('A fatal error happened. See details above.\n'); - process.exit(100); - }); } catch (e) { console.error(red(`Reverting to manual console logging.\nReason: ${e.message}.`)); logger = { diff --git a/docs/specifications/update.md b/docs/specifications/update.md new file mode 100644 index 0000000000..79376fb8f4 --- /dev/null +++ b/docs/specifications/update.md @@ -0,0 +1,191 @@ + +# Update Command + +`ng update` is a new command in the CLI to update one or multiple packages, its peer dependencies, and the peer dependencies that depends on it. + +If there are inconsistencies, for example if peer dependencies cannot be matches by a simple semver range, the tool will error out (and nothing will be committed on the filesystem). + +## Command Line Usage + +```bash +ng update [options] +``` + +You can specify more than one package. Each package follows the convention of `[@scope/]packageName[@version-range-or-dist-tag]`. Packages not found in your dependencies will trigger an error. Any package that has a higher version in your `package.json` will trigger an error. + +| Flag | Argument | Description | +|---|---|---| +| `--all` | `boolean` | If true, implies that all dependencies should be updated. Defaults is false, using dependencies from the command line instead. | +| `--force` | `boolean` | If true, skip the verification step and perform the update even if some peer dependencies would be invalidated. Peer dependencies errors will still be shown as warning. Defaults to false. | +| `--next` | `boolean` | If true, allows version discovery to include Beta and RC. Defaults to false. | +| `--migrate-only` | `boolean` | If true, don't change the `package.json` file, only apply migration scripts. | +| `--from` | `version` | Apply migrations from a certain version number. | +| `--to` | `version` | Apply migrations up to a certain version number (inclusive). By default will update to the installed version. | + +## Details + +The schematic performs the following steps, in order: + +1. Get all installed package names and versions from the `package.json` into `dependencyMap: Map`. +1. From that map, fetch all `package.json` from the NPM repository, which contains all versions, and gather them in a `Map`. + 1. At the same time, update the `Map<>` with the version of the package which is believed to be installed (largest version number matching the version range). + 1. **WARNING**: this might not be the exact installed versions, unfortunately. We should have a proper `package-lock.json` loader, and support `yarn.lock` as well, but these are stretch goals (and where do we stop). +1. For each packages mentioned on the command line, update to the target version (by default largest non-beta non-rc version): + + ```python + # ARGV The packages being requested by the user. + # NPM A map of package name to a map of version to PackageJson structure. + # V A map of package name to available versions. + # PKG A map of package name to PackageJson structure, for the installed versions. + # next A flag for the "--next" command line argument. + + # First add all updating packages' peer dependencies. This should be recursive but simplified + # here for readability. + ARGV += [ NPM[p][max([ v for v in V[p] if (not is_beta(v) or next) ])].peerDependencies + for p in ARGV ] + + for p in ARGV: + x = max([ v for v in V[p] if (not is_beta(v) or next) ]) + + for other in set(PKG.keys()) - set([ p ]): + # Verify all packages' peer dependencies. + if has(other.peerDependencies, p) and !compatible(x, other.peerDependencies[p]): + showError('Cannot update dependency "%s": "%s" is incompatible with the updated dependency' % (x, other)) + + if any( has(other.peerDependencies, peer) and !compatible(x, other.peerDependencies[peer]) + for peer in PKG[p].peerDependencies.keys() ): + showError('Cannot update dependency "%s": "%s" depends on an incompatible peer dependency' % (x, other)) + + update_package_json(p, x) +``` + + + +## Library Developers + +Libraries are responsible for defining their own update schematics. The `ng update` tool will update the package.json, and if it detects am `"ng-update"` key in package.json of the library, will run the update schematic on it (with version information metadata). + +If a library does not define the `"ng-update"` key in their package.json, they are considered not supporting the update workflow and `ng update` is basically equivalent to `npm install`. + +### Migration + +In order to implement migrations in a library, the author must add the `ng-update` key to its `package.json`. This key contains the following fields: + +| Field Name | Type | Description | +|---|---|---| +| `requirements` | `{ [packageName: string]: VersionRange }` | A map of package names to version to check for minimal requirement. If one of the libraries listed here does not match the version range specified in `requirements`, an error will be shown to the user to manually update those libraries. For example, `@angular/core` does not support updates from versions earlier than 5, so this field would be `{ '@angular/core': '>= 5' }`. +| `migrations` | `string` | A relative path (or resolved using Node module resolution) to a Schematics collection definition. | +| `packageGroup` | `string[]` | A list of npm packages that are to be grouped together. When running + +#### Example given: +Library my-lib wants to have 2 steps to update from version 4 -> 4.5 and 4.5 to 5. It would add this information in its `package.json`: + +```json +{ + "ng-update": { + "requirements": { + "my-lib": "^5" + }, + "migrations": "./migrations/migration-collection.json" + } +} +``` + +And create a migration collection (same schema as the Schematics collection): + +```json +{ + "schematics": { + "migration-01": { + "version": "6", + "factory": "./update-6" + }, + "migration-02": { + "version": "6.2", + "factory": "./update-6_2" + }, + "migration-03": { + "version": "6.3", + "factory": "./update-6_3" + }, + "migration-04": { + "version": "7", + "factory": "./update-7" + }, + "migration-05": { + "version": "8", + "factory": "./update-8" + } + } +} +``` + +The update tool would then read the current version of library installed, check against all `version` fields and run the schematics, until it reaches the version required by the user (inclusively). If such a collection is used to update from version 5 to version 7, the `01`, `02`, `03,` and `04` functions would be called. If the current version is 7 and a `--refactor-only` flag is passed, it would run the migration `04` only. More arguments are needed to know from which version you are updating. + +Running `ng update @angular/core` would be the same as `ng generate @angular/core/migrations:migration-01`. + +## Use cases + +### Help + +`ng update`, shows what updates would be applied; + +```sh +$ ng update +We analyzed your package.json, there's some packages to update: + +Name Version Command to update +---------------------------------------------------------------------------- +@angular/cli 1.7.0 > 6.0.0 ng update @angular/cli +@angular/core 5.4.3 > 6.0.1 ng update @angular/core +@angular/material 5.2.1 > 6.0.0 ng update @angular/material +@angular/router 5.4.3 > 6.0.1 ng update @angular/core + +There might be additional packages that are outdated. +``` + +### Simple Multi-steps + +I have a dependency on Angular, Material and CLI. I want to update the CLI, then Angular, then Material in separate steps. + +#### Details +1. `ng update @angular/cli`. +Updates the CLI and packages that have a peer dependencies on the CLI (none), running refactoring tools from CLI 1 to 6. +1. `ng update @angular/core`. +Updates the Core package and all packages that have a peer dependency on it. This can get tricky if `@angular/material` get caught in the update because the version installed does not directly allow the new version of `@angular/core`. In this case + +### Complex Case + +package.json: + +```json +{ + "dependencies": { + "@angular/material": "5.0.0", + "@angular/core": "5.5.5" + } +} +``` + +Commands: + +```bash +ng update @angular/core +``` + +- updates `@angular/core` to the `latest` dist-tag (6.0.0) +- sees that `@angular/material` is not compatible with 6.0.0; **error out.** + +```bash +ng update @angular/material +``` + +- update `@angular/material` to latest version, that should be compatible with the current `@angular/core`. +- if that version is not compatible with you +- tell the user about a higher version that requires an update to `@angular/core`. + + +## Notes + +1. if someone is on CLI 1.5, the command is not supported. The user needs to update to `@angular/cli@latest`, then `ng update @angular/cli`. Post install hook will check versions of cli configuration and show a message to run the `ng update` command. +1. NPM proxies or cache are not supported by the first version of this command. diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js index 3d7b231339..259d6fbfcf 100644 --- a/lib/bootstrap-local.js +++ b/lib/bootstrap-local.js @@ -22,7 +22,13 @@ if (process.env['CODE_COVERAGE'] || process.argv.indexOf('--code-coverage') !== // Check if we need to profile this CLI run. let profiler = null; if (process.env['DEVKIT_PROFILING']) { - profiler = require('v8-profiler'); + try { + profiler = require('v8-profiler'); + } catch (err) { + throw new Error(`Could not require 'v8-profiler'. You must install it separetely with` + + `'npm install v8-profiler --no-save.\n\nOriginal error:\n\n${err}`); + } + profiler.startProfiling(); function exitHandler(options, _err) { @@ -45,8 +51,9 @@ if (process.env['DEVKIT_PROFILING']) { process.on('uncaughtException', exitHandler.bind(null, { exit: true })); } - -Error.stackTraceLimit = Infinity; +if (process.argv.indexOf('--long-stack-trace') !== -1){ + Error.stackTraceLimit = Infinity; +} global._DevKitIsLocal = true; global._DevKitRoot = path.resolve(__dirname, '..'); diff --git a/lib/istanbul-local.js b/lib/istanbul-local.js index ce062344de..ef5782661a 100644 --- a/lib/istanbul-local.js +++ b/lib/istanbul-local.js @@ -18,7 +18,7 @@ exports.codeMap = codeMap; exports.istanbulRequireHook = function(code, filename) { // Skip spec files. - if (filename.match(/_spec\.ts$/)) { + if (filename.match(/_spec(_large)?\.ts$/)) { return code; } const codeFile = codeMap.get(filename); diff --git a/package-lock.json b/package-lock.json index 0a7b955cdb..66deaa64a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,63 +4,183 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@angular/common": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.2.tgz", - "integrity": "sha512-heon7Bdu6SUw/6ma9wEDxrxBJY2V+NSUv7ZVY7HaXESWvxKUGaser5vQIsWghvBg1injSxyw/3BqGFflua/3sQ==", + "@angular/animations": { + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.0.0-rc.3.tgz", + "integrity": "sha512-uUx8n3rnORn3pVb1sPiPYRhCETsaLLLeygPKlfqcTqdO8/hUEdumGI0hfrn0rW8qK2x2Pwxvz2qNPkn4iGgwTg==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "@angular/cdk": { + "version": "6.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-6.0.0-rc.1.tgz", + "integrity": "sha512-V4nJwF9uchgqi1noRSd/Jm0mZ9yzFFeYzZ5mHZ7scXo0c2sXpEclsEjsnzb6YQo4NENEig8qrjOdYI+M7bd5zQ==", "requires": { "tslib": "1.8.1" } }, + "@angular/common": { + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.0.0-rc.3.tgz", + "integrity": "sha512-arc0LoT2opge2CDUXufN+TIjTUUx+N46MSWW1sKiLUzbK38E2nZ4S1RHoVDR6P7c6ruKMmaqZFJkOn6wd5Oi0w==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, "@angular/compiler": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.2.tgz", - "integrity": "sha512-QkliIJJb9J2y4Y1yiSweP1eOStClOOOj46awVQ5wT+WzyvmIVAccx2u+r5TPRu676GlqrFfn6FD+zV6Zw7G+Tw==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.0.0-rc.3.tgz", + "integrity": "sha512-ZRsAtThpSrXKQ0N64Wa7ovDXXQ333uyRKUqApNo0NskvWwURiiBU9gACR4KmJmBRo4PUyITkFnyOM+6QMFDGQQ==", "requires": { - "tslib": "1.8.1" + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } } }, "@angular/compiler-cli": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.2.2.tgz", - "integrity": "sha512-XSojPIMQNvEnYIufTIlrr3GLpr20AUQP0bMzUp4/U/ATWmMWmdNRRG/ys5ncmbgImoAg1nW0hp4bonUSYf9nGQ==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-6.0.0-rc.3.tgz", + "integrity": "sha512-hpVAb3BaX7TaK2iUW91poi1txZv5GeP97qX5c1vTTzGfFveGT5a5zkTee9ihCdelYIl5wBObkRrcXWjVLSXIzw==", "requires": { "chokidar": "1.7.0", "minimist": "1.2.0", - "reflect-metadata": "0.1.10", - "tsickle": "0.26.0" + "reflect-metadata": "0.1.12", + "tsickle": "0.27.5" } }, "@angular/core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.2.tgz", - "integrity": "sha512-SycTFvlJUHzYvqRYM0DQQUewSo0IPL3Vfo9MOwSJvhS5mXCP1+QW0IIhI8CyWy+40L3dIWlYnn0754z5IJikdg==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.0.0-rc.3.tgz", + "integrity": "sha512-zB6bpFNx6Iefko6HKYMSKjyB0XJj8yAgK1G/Ozzb+hbSMmkVi+HetG4v0WXg4sn2mD5NGxj+7qz7tGAzhlBgdA==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "@angular/http": { + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.0.0-rc.3.tgz", + "integrity": "sha512-PN8W2OSeptAIJ5/CeZNGdRiTVHjuYfFSB+ZgBeWhQVBmIV5Lp5iTNDcGslEB9diMkJSNsh/jYtcA0YAFhirYhw==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "@angular/material": { + "version": "6.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-6.0.0-rc.1.tgz", + "integrity": "sha512-53ak9Oi3BJN0ZaYoqWHtgm0dXkmjkZrCWeOuJFy2nM0NtWObv2SUYMd7bss7bSX0GlU/gQD+aBrHF40RwfQjQw==", "requires": { "tslib": "1.8.1" } }, "@angular/platform-browser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.2.tgz", - "integrity": "sha512-jiiEEUiv4oOWtBP96hOnxHOY3ckukfSOaxtw+ENjSPAyv/eRbL1B2LFwIg+HYAFxvK8JOLAYZm3Hg9lpenlBMw==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.0.0-rc.3.tgz", + "integrity": "sha512-uMUAImcIbunqp9DKRSqokeAYWlyYFrqQAUpJNTeksUO9zkP2rPXlUi8ogZtEybHCc9XuoByC+UgC2IOMtmS82g==", "requires": { - "tslib": "1.8.1" + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } } }, "@angular/platform-browser-dynamic": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.2.tgz", - "integrity": "sha512-PCg63japwHw6zGWGHZEpiDKeqPaCbOKnBl7bhRzE5imL+74toyvmE33sp7OzXKGi0mX5mUymfRsvfLdB6khGTQ==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.0.0-rc.3.tgz", + "integrity": "sha512-q5ZUvgGUuIVKx9I9++ovKXRssu5czNzr/1jgfzvh72a2+s5aVyVB8Zd164pdS6GSvi8lyApSNPBnBlRROHklbg==", "requires": { - "tslib": "1.8.1" + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "@angular/platform-server": { + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-6.0.0-rc.3.tgz", + "integrity": "sha512-gIzmma1RWrCwWnaqkOwaSAgNTjRASiCb5iPcOpYoAVwpbkhmRg2UWUYJNI0eDc/p4DHYyM2jET7IdbGrgy3hVw==", + "requires": { + "domino": "2.0.2", + "tslib": "1.9.0", + "xhr2": "0.1.4" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "@angular/router": { + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.0.0-rc.3.tgz", + "integrity": "sha512-H621WrhkGayCZlr7f6V2czVAZPPXCeAXqsGjt5MWgB+MzpPP/+lrqKMhku9ZDE0OrlDucU2g34oipGoh0tJW2g==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } } }, "@angular/service-worker": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-5.2.1.tgz", - "integrity": "sha512-z4+7S5MQYbcp9d4xtZk6tGJY3TkliVTXj/2HdIOXm97wCyDUAdvNLa9sor3N8OtH8IiRgqeXnqq3ehyWvbMZ4g==", + "version": "6.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-6.0.0-rc.3.tgz", + "integrity": "sha512-MQXu3XlqbtSvhBfe07kO7tJo61mMRSvcMMbO8ZmICGX3hXIwJUrAVRA6RzGIR1j67Z4nYkGIs7stZJ3nwF7GzA==", "requires": { - "tslib": "1.8.1" + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } } }, "@ngtools/json-schema": { @@ -68,19 +188,13 @@ "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", "integrity": "sha1-w6DFRNYjkqzCgTpCyKDcb1j4aSI=" }, - "@ngtools/webpack": { - "version": "1.10.0-rc.0", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.10.0-rc.0.tgz", - "integrity": "sha512-GVPBEtg8ScBGwczBej5MgCuBMDA4HoiT3Q1m7J2mXW8+K8GeSsbmSsY0p5ckDHLuEyP8RkbF/NHEkSdhLd9OMQ==", + "@types/body-parser": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", + "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", "requires": { - "chalk": "2.2.2", - "enhanced-resolve": "3.4.1", - "loader-utils": "1.1.0", - "magic-string": "0.22.4", - "semver": "5.4.1", - "source-map": "0.5.7", - "tree-kill": "1.2.0", - "webpack-sources": "1.1.0" + "@types/express": "4.11.1", + "@types/node": "8.9.3" } }, "@types/caseless": { @@ -88,11 +202,6 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" }, - "@types/common-tags": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.4.0.tgz", - "integrity": "sha512-HI1tSO87vmd1sPS3DOVSK4gvVKROvCBFvAnXlLiQtAus/+1xXMQcNyu9TX2ChwRXFeQZeB9+f+nMo99xLd5DdA==" - }, "@types/copy-webpack-plugin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-4.0.1.tgz", @@ -102,16 +211,30 @@ "@types/webpack": "3.8.2" } }, - "@types/denodeify": { - "version": "1.2.31", - "resolved": "https://registry.npmjs.org/@types/denodeify/-/denodeify-1.2.31.tgz", - "integrity": "sha512-Jgy3dvCyIxhNb5RstVJkubeHZifw8KJXca13ov8OO4IqhDLPRHiJJ6VArJbZZ4HuEMJEB83yCuABodNMlYylzQ==" - }, "@types/events": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-1.1.0.tgz", "integrity": "sha512-y3bR98mzYOo0pAZuiLari+cQyiKk3UXRuT45h1RjhfeCzqkjaVsfZJNaxdgtk7/3tzOm1ozLTqEqMP3VbI48jw==" }, + "@types/express": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.11.1.tgz", + "integrity": "sha512-ttWle8cnPA5rAelauSWeWJimtY2RsUf2aspYZs7xPHiWgOlPn6nnUfBMtrkcnjFJuIHJF4gNOdVvpLK2Zmvh6g==", + "requires": { + "@types/body-parser": "1.16.8", + "@types/express-serve-static-core": "4.11.1", + "@types/serve-static": "1.13.1" + } + }, + "@types/express-serve-static-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz", + "integrity": "sha512-EehCl3tpuqiM8RUb+0255M8PhhSwTtLfmO7zBBdv0ay/VTd/zmrqDfQdZFsa5z/PVMbH2yCMZPXsnrImpATyIw==", + "requires": { + "@types/events": "1.1.0", + "@types/node": "8.9.3" + } + }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", @@ -149,6 +272,11 @@ "@types/webpack": "3.8.2" } }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -190,6 +318,15 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.4.0.tgz", "integrity": "sha512-PBHCvO98hNec9A491vBbh0ZNDOVxccwKL1u2pm6fs9oDgm7SEnw0lEHqHfjsYryDxnE3zaf7LvERWEXjOp1hig==" }, + "@types/serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", + "requires": { + "@types/express-serve-static-core": "4.11.1", + "@types/mime": "2.0.0" + } + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -281,11 +418,18 @@ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" }, "acorn-dynamic-import": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", - "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", "requires": { - "acorn": "4.0.13" + "acorn": "5.5.3" + }, + "dependencies": { + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" + } } }, "addressparser": { @@ -332,9 +476,9 @@ } }, "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz", + "integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=" }, "align-text": { "version": "0.1.4", @@ -346,16 +490,48 @@ "repeat-string": "1.6.1" } }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" - }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "requires": { + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -591,15 +767,15 @@ "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" }, "autoprefixer": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.3.tgz", - "integrity": "sha512-dqzVGiz3v934+s3YZA6nk7tAs9xuTz5wMJbX1M+L4cY/MTNkOUqP61c1GWkEVlUL/PEy1pKRSCFuoRZrXYx9qA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.1.0.tgz", + "integrity": "sha512-b6mjq6VZ0guW6evRkKXL5sSSvIXICAE9dyWReZ3l/riidU7bVaJMe5cQ512SmaLA4Pvgnhi5MFsMs/Mvyh9//Q==", "requires": { - "browserslist": "2.10.0", - "caniuse-lite": "1.0.30000784", + "browserslist": "3.1.2", + "caniuse-lite": "1.0.30000813", "normalize-range": "0.1.2", "num2fraction": "1.2.2", - "postcss": "6.0.16", + "postcss": "6.0.19", "postcss-value-parser": "3.3.0" } }, @@ -755,14 +931,22 @@ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "requires": { "cache-base": "1.0.1", - "class-utils": "0.3.5", + "class-utils": "0.3.6", "component-emitter": "1.2.1", "define-property": "1.0.0", "isobject": "3.0.1", - "mixin-deep": "1.3.0", + "mixin-deep": "1.3.1", "pascalcase": "0.1.1" }, "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", @@ -912,7 +1096,7 @@ "deep-equal": "1.0.1", "dns-equal": "1.0.0", "dns-txt": "2.0.2", - "multicast-dns": "6.2.1", + "multicast-dns": "6.2.3", "multicast-dns-service-types": "1.1.0" } }, @@ -929,6 +1113,59 @@ "hoek": "2.16.3" } }, + "bootstrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0.tgz", + "integrity": "sha512-gulJE5dGFo6Q61V/whS6VM4WIyrlydXfCgkE+Gxe5hjrJ8rXLLZlALq7zq2RPhOc45PSwQpJkrTnc2KgD6cvmA==" + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "requires": { + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.2.2", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -971,6 +1208,13 @@ "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", "requires": { "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } } }, "browserify": { @@ -1010,7 +1254,7 @@ "querystring-es3": "0.2.1", "read-only-stream": "2.0.0", "readable-stream": "2.3.3", - "resolve": "1.1.7", + "resolve": "1.5.0", "shasum": "1.0.2", "shell-quote": "1.6.1", "stream-browserify": "2.0.1", @@ -1124,12 +1368,12 @@ } }, "browserslist": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.10.0.tgz", - "integrity": "sha512-WyvzSLsuAVPOjbljXnyeWl14Ae+ukAT8MUuagKVzIDvwBxl4UAwD1xqtyQs2eWYPGUKMeC3Ol62goqYuKqTTcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.1.2.tgz", + "integrity": "sha512-iO5MiK7MZXejqfnCK8onktxxb+mcW+KMiL/5gGF/UCWvVgPzbgbkA5cyYfqj/IIHHo7X1z0znrSHPw9AIfpvrw==", "requires": { - "caniuse-lite": "1.0.30000784", - "electron-to-chromium": "1.3.28" + "caniuse-lite": "1.0.30000813", + "electron-to-chromium": "1.3.37" } }, "buffer": { @@ -1141,6 +1385,11 @@ "ieee754": "1.1.8" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-indexof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", @@ -1182,23 +1431,23 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "cacache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.1.tgz", - "integrity": "sha512-dRHYcs9LvG9cHgdPzjiI+/eS7e1xRhULrcyOx04RZQsszNJXU2SL9CyG60yLnge282Qq5nwTv+ieK2fH+WPZmA==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", "requires": { "bluebird": "3.5.1", "chownr": "1.0.1", "glob": "7.1.2", "graceful-fs": "4.1.11", "lru-cache": "4.1.1", - "mississippi": "1.3.0", + "mississippi": "2.0.0", "mkdirp": "0.5.1", "move-concurrently": "1.0.1", "promise-inflight": "1.0.1", "rimraf": "2.6.2", - "ssri": "5.0.0", + "ssri": "5.2.4", "unique-filename": "1.1.0", - "y18n": "3.2.1" + "y18n": "4.0.0" }, "dependencies": { "rimraf": { @@ -1208,6 +1457,11 @@ "requires": { "glob": "7.1.2" } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" } } }, @@ -1234,6 +1488,43 @@ } } }, + "cache-loader": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-1.2.2.tgz", + "integrity": "sha512-rsGh4SIYyB9glU+d0OcHwiXHXBoUgDhHZaQ1KAbiXqfz1CDPxtTboh1gPbJ0q2qdO8a9lfcjgC5CJ2Ms32y5bw==", + "requires": { + "loader-utils": "1.1.0", + "mkdirp": "0.5.1", + "neo-async": "2.5.0", + "schema-utils": "0.4.5" + }, + "dependencies": { + "ajv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz", + "integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=", + "requires": { + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz", + "integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=" + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "requires": { + "ajv": "6.2.0", + "ajv-keywords": "3.1.0" + } + } + } + }, "cached-path-relative": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz", @@ -1267,37 +1558,15 @@ "map-obj": "1.0.1" } }, - "caniuse-api": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", - "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", - "requires": { - "browserslist": "1.7.7", - "caniuse-db": "1.0.30000780", - "lodash.memoize": "4.1.2", - "lodash.uniq": "4.5.0" - }, - "dependencies": { - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "requires": { - "caniuse-db": "1.0.30000780", - "electron-to-chromium": "1.3.28" - } - } - } - }, - "caniuse-db": { - "version": "1.0.30000780", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000780.tgz", - "integrity": "sha1-jRl3Vh0A/w8O0ra2YUAyirRQTAo=" - }, "caniuse-lite": { - "version": "1.0.30000784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000784.tgz", - "integrity": "sha1-EpztdOmhKApEGIC2zSvOMO9Z5sA=" + "version": "1.0.30000813", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000813.tgz", + "integrity": "sha512-A8ITSmH5SFdMFdC704ggjg+x2z5PzQmVlG8tavwnfvbC33Q1UYrj0+G+Xm0SNAnd4He36fwUE/KEWytOEchw+A==" + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" }, "caseless": { "version": "0.12.0", @@ -1308,6 +1577,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "optional": true, "requires": { "align-text": "0.1.4", "lazy-cache": "1.0.4" @@ -1359,6 +1629,11 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" }, + "chrome-trace-event": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz", + "integrity": "sha1-kPNohdU0WlBiEzLwcXtZWIPV2YI=" + }, "ci-info": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.2.tgz", @@ -1374,51 +1649,18 @@ } }, "circular-dependency-plugin": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-4.3.0.tgz", - "integrity": "sha512-L3W9L1S0wC64rq+QSaZzmWnJW7cVBgimxI2lNEFEX5biwlRG8EHRM68JFi+CX5ZkCGUWJHIpnhdVs181Zlq3wA==" - }, - "clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "requires": { - "chalk": "1.1.3" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.0.0.tgz", + "integrity": "sha512-mJzk5D+Uy/bVVk89GDfdgINMc03FaqfpHg+nx/D8l2okD48FPmB5nBlpXj6HcmBkCv5AhY5qEfvCXmszc9vXpA==" }, "class-utils": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.5.tgz", - "integrity": "sha1-F+eTEDdQ+WJ7IXbqNM/RtWWQPIA=", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "requires": { "arr-union": "3.1.0", "define-property": "0.2.5", "isobject": "3.0.1", - "lazy-cache": "2.0.2", "static-extend": "0.1.2" }, "dependencies": { @@ -1430,17 +1672,53 @@ "is-descriptor": "0.1.6" } }, - "is-descriptor": { + "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "isobject": { + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" @@ -1449,29 +1727,27 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - }, - "lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "requires": { - "set-getter": "0.1.0" - } } } }, "clean-css": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.9.tgz", - "integrity": "sha1-Nc7ornaHpJuYA09w3gDE7dOCYwE=", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", "requires": { "source-map": "0.5.7" } }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "optional": true, "requires": { "center-align": "0.1.3", "right-align": "0.1.3", @@ -1481,58 +1757,30 @@ "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "optional": true } } }, "clone": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", - "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=" - }, - "clone-deep": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.3.0.tgz", - "integrity": "sha1-NIxhrpzb4O3+BT2R/0zFIdeQ7eg=", - "requires": { - "for-own": "1.0.0", - "is-plain-object": "2.0.4", - "kind-of": "3.2.2", - "shallow-clone": "0.1.2" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "requires": { - "for-in": "1.0.2" - } - } - } + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, - "coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", - "requires": { - "q": "1.5.1" - } - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codelyzer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.0.2.tgz", - "integrity": "sha512-nYwOr49+IV09e7C4aXkVALRz0+XpHqZiUUcxHuDZH4xP1FBcHINyr3qvVhv5Gfm7XRmoLx32tsIhrQhW/gBcog==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.2.1.tgz", + "integrity": "sha512-CKwfgpfkqi9dyzy4s6ELaxJ54QgJ6A8iTSsM4bzHbLuTpbKncvNc3DUlCvpnkHBhK47gEf4qFsWoYqLrJPhy6g==", "requires": { "app-root-path": "2.0.1", "css-selector-tokenizer": "0.7.0", @@ -1551,16 +1799,6 @@ "object-visit": "1.0.1" } }, - "color": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", - "requires": { - "clone": "1.0.3", - "color-convert": "1.9.1", - "color-string": "0.3.0" - } - }, "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", @@ -1574,24 +1812,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", - "requires": { - "color-name": "1.1.3" - } - }, - "colormin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", - "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", - "requires": { - "color": "0.11.4", - "css-color-names": "0.0.4", - "has": "1.0.1" - } - }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -1636,13 +1856,10 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" }, - "common-tags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.5.1.tgz", - "integrity": "sha512-NrUYGY5TApAk9KB+IZXkR3GR4tA3g26HDsoiGt4kCMHZ727gOGkC+UNfq0Z22jE15bLkc/6RV5Jw1RBW6Usg6A==", - "requires": { - "babel-runtime": "6.26.0" - } + "commenting": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.0.5.tgz", + "integrity": "sha512-U7qGbcDLSNpOcV3RQRKHp7hFpy9WUmfawbkPdS4R2RhrSu4dOF85QQpx/Zjcv7uLF6tWSUKEKUIkxknPCrVjwg==" }, "commondir": { "version": "1.0.1", @@ -1674,21 +1891,28 @@ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compressible": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz", - "integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", + "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", "requires": { - "mime-db": "1.30.0" + "mime-db": "1.33.0" + }, + "dependencies": { + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + } } }, "compression": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", - "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "requires": { - "accepts": "1.3.4", + "accepts": "1.3.5", "bytes": "3.0.0", - "compressible": "2.0.12", + "compressible": "2.0.13", "debug": "2.6.9", "on-headers": "1.0.1", "safe-buffer": "5.1.1", @@ -1696,13 +1920,26 @@ }, "dependencies": { "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "requires": { - "mime-types": "2.1.17", + "mime-types": "2.1.18", "negotiator": "0.6.1" } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } } } }, @@ -1712,15 +1949,38 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-gslSSJx03QKa59cIKqeJO9HQ/WZMotvYJCuaUULrLpjj8oG40kV2Z+gz82pVxlTkOADi4PJxQPPfhl1ELYrrXw==", "requires": { "inherits": "2.0.3", "readable-stream": "2.3.3", "typedarray": "0.0.6" } }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.2.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "1.0.1" + } + } + } + }, "connect": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.5.tgz", @@ -1960,17 +2220,17 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "copy-webpack-plugin": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.4.1.tgz", - "integrity": "sha512-ojaz8MpS3zoLJT/JbYMusYM+dCEArhW24hGAUPYPydTCS+87NFh2TWr85sywG3So4Q4E68QoerqQ+Ns1g0fhDg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.0.tgz", + "integrity": "sha512-ROQ85fWKuhJfUkBTdHvfV+Zv6Ltm3G/vPVFdLPFwzWzd9RUY1yLw3rt6FmKK2PaeNQCNvmwgFhuarkjuV4PVDQ==", "requires": { - "cacache": "10.0.1", + "cacache": "10.0.4", "find-cache-dir": "1.0.0", "globby": "7.1.1", "is-glob": "4.0.0", - "loader-utils": "0.2.17", + "loader-utils": "1.1.0", "minimatch": "3.0.4", - "p-limit": "1.1.0", + "p-limit": "1.2.0", "serialize-javascript": "1.4.0" }, "dependencies": { @@ -1986,17 +2246,6 @@ "requires": { "is-extglob": "2.1.1" } - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "requires": { - "big.js": "3.2.0", - "emojis-list": "2.1.0", - "json5": "0.5.1", - "object-assign": "4.1.1" - } } } }, @@ -2024,6 +2273,24 @@ "require-from-string": "1.2.1" } }, + "cpx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", + "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", + "requires": { + "babel-runtime": "6.26.0", + "chokidar": "1.7.0", + "duplexer": "0.1.1", + "glob": "7.1.2", + "glob2base": "0.0.12", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "resolve": "1.5.0", + "safe-buffer": "5.1.1", + "shell-quote": "1.6.1", + "subarg": "1.0.0" + } + }, "create-ecdh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", @@ -2033,6 +2300,14 @@ "elliptic": "6.4.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "requires": { + "capture-stack-trace": "1.0.0" + } + }, "create-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", @@ -2092,68 +2367,10 @@ "randomfill": "1.0.3" } }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" - }, - "css-loader": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz", - "integrity": "sha512-GxMpax8a/VgcfRrVy0gXD6yLd5ePYbXX/5zGgTVYp4wXtJklS8Z2VaUArJgc//f6/Dzil7BaJObdSv8eKKCPgg==", - "requires": { - "babel-code-frame": "6.26.0", - "css-selector-tokenizer": "0.7.0", - "cssnano": "3.10.0", - "icss-utils": "2.1.0", - "loader-utils": "1.1.0", - "lodash.camelcase": "4.3.0", - "object-assign": "4.1.1", - "postcss": "5.2.18", - "postcss-modules-extract-imports": "1.1.0", - "postcss-modules-local-by-default": "1.2.0", - "postcss-modules-scope": "1.1.0", - "postcss-modules-values": "1.3.0", - "postcss-value-parser": "3.3.0", - "source-list-map": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } - } + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" }, "css-parse": { "version": "1.7.0", @@ -2199,113 +2416,6 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" }, - "cssnano": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", - "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", - "requires": { - "autoprefixer": "6.7.7", - "decamelize": "1.2.0", - "defined": "1.0.0", - "has": "1.0.1", - "object-assign": "4.1.1", - "postcss": "5.2.18", - "postcss-calc": "5.3.1", - "postcss-colormin": "2.2.2", - "postcss-convert-values": "2.6.1", - "postcss-discard-comments": "2.0.4", - "postcss-discard-duplicates": "2.1.0", - "postcss-discard-empty": "2.1.0", - "postcss-discard-overridden": "0.1.1", - "postcss-discard-unused": "2.2.3", - "postcss-filter-plugins": "2.0.2", - "postcss-merge-idents": "2.1.7", - "postcss-merge-longhand": "2.0.2", - "postcss-merge-rules": "2.1.2", - "postcss-minify-font-values": "1.0.5", - "postcss-minify-gradients": "1.0.5", - "postcss-minify-params": "1.2.2", - "postcss-minify-selectors": "2.1.1", - "postcss-normalize-charset": "1.1.1", - "postcss-normalize-url": "3.0.8", - "postcss-ordered-values": "2.2.3", - "postcss-reduce-idents": "2.4.0", - "postcss-reduce-initial": "1.0.1", - "postcss-reduce-transforms": "1.0.4", - "postcss-svgo": "2.1.6", - "postcss-unique-selectors": "2.0.2", - "postcss-value-parser": "3.3.0", - "postcss-zindex": "2.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", - "requires": { - "browserslist": "1.7.7", - "caniuse-db": "1.0.30000780", - "normalize-range": "0.1.2", - "num2fraction": "1.2.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - } - }, - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "requires": { - "caniuse-db": "1.0.30000780", - "electron-to-chromium": "1.3.28" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } - } - }, - "csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", - "requires": { - "clap": "1.2.3", - "source-map": "0.5.7" - } - }, "cuint": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", @@ -2329,14 +2439,6 @@ "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "requires": { - "es5-ext": "0.10.37" - } - }, "dargs": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", @@ -2417,8 +2519,7 @@ "deep-extend": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "optional": true + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" }, "deep-is": { "version": "0.1.3", @@ -2443,11 +2544,19 @@ } }, "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "requires": { - "is-descriptor": "1.0.1" + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } } }, "defined": { @@ -2513,11 +2622,6 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=" - }, "depd": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", @@ -2556,12 +2660,6 @@ "repeating": "2.0.1" } }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true - }, "detect-node": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", @@ -2643,9 +2741,9 @@ "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" }, "dns-packet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.2.2.tgz", - "integrity": "sha512-kN+DjfGF7dJGUL7nWRktL9Z18t1rWP3aQlyZdY8XlpvU3Nc6GeFTQApftcjtWKxAZfiggZSGrCEoszNgvnpwDg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", "requires": { "ip": "1.1.5", "safe-buffer": "5.1.1" @@ -2726,6 +2824,11 @@ "domelementtype": "1.3.0" } }, + "domino": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.0.2.tgz", + "integrity": "sha512-vzykUakUw5s1p0RrN/vI2sShYo3pLRy/z7PM1PuOIZIlMOJ0XfOnrckGE5f4MxIQVe5XcrH7yG9mR+l77mgLVA==" + }, "domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", @@ -2749,6 +2852,11 @@ "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=", "optional": true }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -2757,12 +2865,17 @@ "readable-stream": "2.3.3" } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, "duplexify": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz", - "integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz", + "integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==", "requires": { - "end-of-stream": "1.4.0", + "end-of-stream": "1.4.1", "inherits": "2.0.3", "readable-stream": "2.3.3", "stream-shift": "1.0.0" @@ -2788,9 +2901,9 @@ "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" }, "electron-to-chromium": { - "version": "1.3.28", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz", - "integrity": "sha1-jdTmRYCGZE6fnwoc8y4qH53/2e4=" + "version": "1.3.37", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.37.tgz", + "integrity": "sha1-SpJzTgBEyM8LFVO+V+riGkxuX6s=" }, "elliptic": { "version": "6.4.0", @@ -2817,9 +2930,9 @@ "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" }, "end-of-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", - "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { "once": "1.4.0" } @@ -2869,14 +2982,13 @@ } }, "enhanced-resolve": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", - "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz", + "integrity": "sha512-jox/62b2GofV1qTUQTMPEJSDIGycS43evqYzD/KVtEb9OCoki9cnacUPxCrZa7JfPzZSYOCZhu9O9luaMxAX8g==", "requires": { "graceful-fs": "4.1.11", "memory-fs": "0.4.1", - "object-assign": "4.1.1", - "tapable": "0.2.8" + "tapable": "1.0.0" } }, "ent": { @@ -2927,75 +3039,11 @@ "is-symbol": "1.0.1" } }, - "es5-ext": { - "version": "0.10.37", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz", - "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=", - "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-symbol": "3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-iterator": "2.0.3", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, "es6-promise": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=" }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" - } - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3029,14 +3077,12 @@ } } }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.2.0", + "esrecurse": "4.2.1", "estraverse": "4.2.0" }, "dependencies": { @@ -3053,12 +3099,11 @@ "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" }, "esrecurse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", - "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "requires": { - "estraverse": "4.2.0", - "object-assign": "4.1.1" + "estraverse": "4.2.0" }, "dependencies": { "estraverse": { @@ -3073,6 +3118,11 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" }, + "estree-walker": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.3.1.tgz", + "integrity": "sha1-5rGlHPcpJSTnI3wxLl/mZgwc4ao=" + }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -3083,15 +3133,6 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.37" - } - }, "eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", @@ -3205,21 +3246,12 @@ "fill-range": "2.2.3" } }, - "exports-loader": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.6.4.tgz", - "integrity": "sha1-1w/GEhl1s1/BKDDPUnVL4nQPyIY=", - "requires": { - "loader-utils": "1.1.0", - "source-map": "0.5.7" - } - }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", "requires": { - "accepts": "1.3.4", + "accepts": "1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.2", "content-disposition": "0.5.2", @@ -3238,7 +3270,7 @@ "on-finished": "2.3.0", "parseurl": "1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", + "proxy-addr": "2.0.3", "qs": "6.5.1", "range-parser": "1.2.0", "safe-buffer": "5.1.1", @@ -3252,11 +3284,11 @@ }, "dependencies": { "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "requires": { - "mime-types": "2.1.17", + "mime-types": "2.1.18", "negotiator": "0.6.1" } }, @@ -3279,6 +3311,19 @@ "unpipe": "1.0.0" } }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", @@ -3302,11 +3347,22 @@ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" }, "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "requires": { - "is-extendable": "0.1.1" + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } } }, "extglob": { @@ -3317,27 +3373,6 @@ "is-extglob": "1.0.0" } }, - "extract-text-webpack-plugin": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz", - "integrity": "sha512-bt/LZ4m5Rqt/Crl2HiKuAl/oqg0psx1tsTLkvWbJen1CtD+fftkZhMaQ9HOtY2gWsl2Wq+sABmMVi9z3DhKWQQ==", - "requires": { - "async": "2.6.0", - "loader-utils": "1.1.0", - "schema-utils": "0.3.0", - "webpack-sources": "1.1.0" - }, - "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "4.17.4" - } - } - } - }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -3372,12 +3407,33 @@ } }, "file-loader": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.5.tgz", - "integrity": "sha512-RzGHDatcVNpGISTvCpfUfOGpYuSR7HSsSg87ki+wF6rw1Hm0RALPTiAdsxAq1UwLf0RRhbe22/eHK6nhXspiOQ==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", "requires": { "loader-utils": "1.1.0", - "schema-utils": "0.3.0" + "schema-utils": "0.4.5" + }, + "dependencies": { + "ajv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz", + "integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=", + "requires": { + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "requires": { + "ajv": "6.2.0", + "ajv-keywords": "3.1.0" + } + } } }, "file-uri-to-path": { @@ -3439,10 +3495,20 @@ "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", "requires": { "commondir": "1.0.1", - "make-dir": "1.1.0", + "make-dir": "1.2.0", "pkg-dir": "2.0.0" } }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=" + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=" + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -3452,11 +3518,6 @@ "pinkie-promise": "2.0.1" } }, - "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=" - }, "flush-write-stream": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.2.tgz", @@ -3475,6 +3536,11 @@ "debug": "2.6.9" } }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -3543,6 +3609,24 @@ "null-check": "1.0.0" } }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "2.2.1" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -4359,17 +4443,6 @@ "rimraf": "2.2.8" } }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, "ftp": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", @@ -4576,6 +4649,22 @@ "is-glob": "2.0.1" } }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "requires": { + "find-index": "0.1.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "requires": { + "ini": "1.3.5" + } + }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", @@ -4611,6 +4700,24 @@ "minimatch": "3.0.4" } }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.1", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -4839,63 +4946,64 @@ "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", "requires": { "inherits": "2.0.3", - "obuf": "1.1.1", + "obuf": "1.1.2", "readable-stream": "2.3.3", - "wbuf": "1.7.2" + "wbuf": "1.7.3" } }, - "html-comment-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", - "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=" - }, "html-entities": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" }, "html-minifier": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.7.tgz", - "integrity": "sha512-GISXn6oKDo7+gVpKOgZJTbHMCUI2TSGfpg/8jgencWhWJsvEmsvp3M8emX7QocsXsYznWloLib3OeSfeyb/ewg==", + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.9.tgz", + "integrity": "sha512-EZqO91XJwkj8BeLx9C12sKB/AHoTANaZax39vEOP9f/X/9jgJ3r1O2+neabuHqpz5kJO71TapP9JrtCY39su1A==", "requires": { "camel-case": "3.0.0", - "clean-css": "4.1.9", - "commander": "2.12.2", + "clean-css": "4.1.11", + "commander": "2.14.1", "he": "1.1.1", "ncname": "1.0.0", "param-case": "2.1.1", "relateurl": "0.2.7", - "uglify-js": "3.2.1" + "uglify-js": "3.3.12" }, "dependencies": { + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "uglify-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.2.1.tgz", - "integrity": "sha512-BhZTJPmOKPSUcjnx2nlfaOQKHLyjjT4HFyzFWF1BUErx9knJNpdW94ql5o8qVxeNL+8IAWjEjnPvASH2yZnkMg==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.12.tgz", + "integrity": "sha512-4jxrTXlV0HaXTsNILfXW0eey7Qo8qHYM6ih5ZNh45erDWU2GHmKDmekwBTskDb12h+kdd2DBvdzqVb47YzNmTA==", "requires": { - "commander": "2.12.2", + "commander": "2.14.1", "source-map": "0.6.1" } } } }, "html-webpack-plugin": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz", - "integrity": "sha1-f5xCG36pHsRg9WUn1430hO51N9U=", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.0.6.tgz", + "integrity": "sha1-01sEUqrhKaip8/rEShaaYl2M8/o=", "requires": { - "bluebird": "3.5.1", - "html-minifier": "3.5.7", + "html-minifier": "3.5.9", "loader-utils": "0.2.17", "lodash": "4.17.4", "pretty-error": "2.1.1", - "toposort": "1.0.6" + "tapable": "1.0.0", + "toposort": "1.0.6", + "util.promisify": "1.0.0" }, "dependencies": { "loader-utils": { @@ -4975,9 +5083,9 @@ } }, "http-parser-js": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", - "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=" + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.11.tgz", + "integrity": "sha512-QCR5O2AjjMW8Mo4HyI1ctFcv+O99j/0g367V3YoVnrNw5hkDvAWZD0lWGcc+F4yN3V55USPCVix4efb75HxFfA==" }, "http-proxy": { "version": "1.16.2", @@ -5090,19 +5198,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, - "icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=" - }, - "icss-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", - "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", - "requires": { - "postcss": "6.0.16" - } - }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", @@ -5129,6 +5224,11 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, "import-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", @@ -5156,11 +5256,6 @@ "repeating": "2.0.1" } }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" - }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -5191,6 +5286,11 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, + "injection-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.2.1.tgz", + "integrity": "sha512-zHI+E+dM0PXix5FFTO1Y4/UOyAzE7zG1l/QwAn4jchTThOoBq+UYRFK4AVG7lQgFL+go62SbrzSsjXy9DFEZUg==" + }, "inline-source-map": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", @@ -5252,11 +5352,6 @@ "meow": "3.7.0" } }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" - }, "invariant": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", @@ -5277,21 +5372,23 @@ "optional": true }, "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" }, "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { - "kind-of": "3.2.2" + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } } }, "is-arrayish": { @@ -5334,11 +5431,18 @@ } }, "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { - "kind-of": "3.2.2" + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } } }, "is-date-object": { @@ -5347,19 +5451,19 @@ "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.1.tgz", - "integrity": "sha512-G3fFVFTqfaqu7r4YuSBHKBAuOaLz8Sy7ekklUpFEliaLMP1Y2ZjoN9jS62YWCAPQrQpMUQSitRlrzibbuCZjdA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" }, "dependencies": { "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -5415,6 +5519,20 @@ "is-extglob": "1.0.0" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "requires": { + "global-dirs": "0.1.1", + "is-path-inside": "1.0.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + }, "is-my-json-valid": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", @@ -5426,6 +5544,11 @@ "xtend": "4.0.1" } }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=" + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -5440,20 +5563,17 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, "is-odd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-1.0.0.tgz", - "integrity": "sha1-O4qTLrAos3dcObsJ6RdnrM22kIg=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", "requires": { - "is-number": "3.0.0" + "is-number": "4.0.0" }, "dependencies": { "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" } } }, @@ -5478,11 +5598,6 @@ "path-is-inside": "1.0.2" } }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -5513,6 +5628,11 @@ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -5521,6 +5641,11 @@ "has": "1.0.1" } }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -5531,14 +5656,6 @@ "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" }, - "is-svg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", - "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", - "requires": { - "html-comment-regex": "1.1.1" - } - }, "is-symbol": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", @@ -5562,6 +5679,11 @@ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -5628,6 +5750,11 @@ "path-is-absolute": "1.0.1" } }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -5663,6 +5790,24 @@ } } }, + "istanbul-instrumenter-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz", + "integrity": "sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w==", + "requires": { + "convert-source-map": "1.5.1", + "istanbul-lib-instrument": "1.9.1", + "loader-utils": "1.1.0", + "schema-utils": "0.3.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + } + } + }, "istanbul-lib-coverage": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", @@ -5767,6 +5912,11 @@ "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=" }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + }, "js-base64": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", @@ -5804,16 +5954,10 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==" - }, "json-parse-better-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", - "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", - "dev": true + "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==" }, "json-schema": { "version": "0.2.3", @@ -5849,6 +5993,14 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -5983,7 +6135,7 @@ "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-1.0.1.tgz", "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=", "requires": { - "resolve": "1.1.7" + "resolve": "1.5.0" } }, "karma-coverage-istanbul-reporter": { @@ -6056,10 +6208,19 @@ } } }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "requires": { + "package-json": "4.0.1" + } + }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "optional": true }, "lcid": { "version": "1.0.0", @@ -6070,9 +6231,9 @@ } }, "less": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", - "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/less/-/less-3.0.1.tgz", + "integrity": "sha512-qUR4uNv88/c0mpnGOULgMLRXXSD6X0tYo4cVrokzsvn68+nuj8rskInCSe2eLAVYWGD/oAlq8P7J/FeZ/euKiw==", "requires": { "errno": "0.1.4", "graceful-fs": "4.1.11", @@ -6117,19 +6278,19 @@ } }, "less-loader": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.0.5.tgz", - "integrity": "sha1-rhVadAbKxqzSk9eFWH/P8PR4xN0=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz", + "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==", "requires": { "clone": "2.1.1", "loader-utils": "1.1.0", - "pify": "2.3.0" + "pify": "3.0.0" }, "dependencies": { - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=" + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" } } }, @@ -6294,9 +6455,9 @@ } }, "license-webpack-plugin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.1.1.tgz", - "integrity": "sha512-TjKOyiC0exqd4Idy/4M8/DETR22dXBZks387DuS5LbslxHiMRXGx/Q2F/j9IUtvEoH5uFvt72vRgk/G6f8j3Dg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-1.2.3.tgz", + "integrity": "sha512-+sie46vNe5L48N94LEzEvreJqAdi+N3x3mXUx+iujuAmftWdJUh68RSDPgWK3DRJuu50dwiyH7MdVAx95zfKQA==", "requires": { "ejs": "2.5.7" } @@ -6367,21 +6528,11 @@ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" - }, "lodash.mergewith": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", @@ -6409,10 +6560,13 @@ "lodash._reinterpolate": "3.0.0" } }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "2.2.2" + } }, "log4js": { "version": "2.3.12", @@ -6547,9 +6701,14 @@ } }, "loglevel": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.0.tgz", - "integrity": "sha1-rgyqVhERSYxboTcj1vtjHSQAOTQ=" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=" + }, + "loglevelnext": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.3.tgz", + "integrity": "sha512-OCxd/b78TijTB4b6zVqLbMrxhebyvdZKwqpL0VHUZ0pYhavXaPD4l6Xrr4n5xqTYWiqtb0i7ikSoJY/myQ/Org==" }, "longest": { "version": "1.0.1", @@ -6578,6 +6737,11 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, "lru-cache": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", @@ -6587,15 +6751,10 @@ "yallist": "2.1.2" } }, - "macaddress": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", - "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=" - }, "magic-string": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.4.tgz", - "integrity": "sha512-kxBL06p6iO2qPBHsqGK2b3cRwiRGpnmSuVWNhwHcMX7qJOUr1HvricYP1LZOCdkQBUp0jiWg2d6WJwR3vYgByw==", + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "requires": { "vlq": "0.2.3" } @@ -6660,9 +6819,9 @@ } }, "make-dir": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", - "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", + "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", "requires": { "pify": "3.0.0" }, @@ -6697,10 +6856,10 @@ "object-visit": "1.0.1" } }, - "math-expression-evaluator": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", - "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + "material-design-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", + "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=" }, "md5.js": { "version": "1.3.4", @@ -6732,7 +6891,7 @@ "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", "requires": { - "mimic-fn": "1.1.0" + "mimic-fn": "1.2.0" } }, "memory-fs": { @@ -6819,9 +6978,18 @@ } }, "mimic-fn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", - "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "mini-css-extract-plugin": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.3.0.tgz", + "integrity": "sha512-xWifHy3fqq0HZeEZ0WTi22tek85YQqNFlGxtvSXJXBi1O6XgqKMyK6fsupSBaaIsyBdfpr9QsG93hrWu13pruQ==", + "requires": { + "loader-utils": "1.1.0", + "webpack-sources": "1.1.0" + } }, "minimalistic-assert": { "version": "1.0.0", @@ -6870,26 +7038,26 @@ } }, "mississippi": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-1.3.0.tgz", - "integrity": "sha1-0gFYPrEjJ+PFwWQqQEqcrPlONPU=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", "requires": { - "concat-stream": "1.6.0", - "duplexify": "3.5.1", - "end-of-stream": "1.4.0", + "concat-stream": "1.6.1", + "duplexify": "3.5.4", + "end-of-stream": "1.4.1", "flush-write-stream": "1.0.2", "from2": "2.3.0", "parallel-transform": "1.1.0", - "pump": "1.0.3", - "pumpify": "1.3.5", + "pump": "2.0.1", + "pumpify": "1.4.0", "stream-each": "1.2.2", "through2": "2.0.3" } }, "mixin-deep": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.0.tgz", - "integrity": "sha512-dgaCvoh6i1nosAUBKb0l0pfJ78K8+S9fluyIR2YvAeUD/QuMahnFnF3xYty5eYXMjhGSsB0DsW6A0uAZyetoAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "requires": { "for-in": "1.0.2", "is-extendable": "1.0.1" @@ -6956,7 +7124,7 @@ "inherits": "2.0.3", "parents": "1.0.1", "readable-stream": "2.3.3", - "resolve": "1.1.7", + "resolve": "1.5.0", "stream-combiner2": "1.1.1", "subarg": "1.0.0", "through2": "2.0.3", @@ -6995,6 +7163,11 @@ } } }, + "moment": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", + "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7024,12 +7197,12 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multicast-dns": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.1.tgz", - "integrity": "sha512-uV3/ckdsffHx9IrGQrx613mturMdMqQ06WTq+C09NsStJ9iNG6RcUWgPKs1Rfjy+idZT6tfQoXEusGNnEZhT3w==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", "requires": { - "dns-packet": "1.2.2", - "thunky": "0.1.0" + "dns-packet": "1.3.1", + "thunky": "1.0.2" } }, "multicast-dns-service-types": { @@ -7043,21 +7216,22 @@ "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" }, "nanomatch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.6.tgz", - "integrity": "sha512-WJ6XTCbvWXUFPbi/bDwKcYkCeOGUHzaJj72KbuPqGn78Ba/F5Vu26Zlo6SuMQbCIst1RGKL1zfWBCOGAlbRLAg==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", "requires": { "arr-diff": "4.0.0", "array-unique": "0.3.2", - "define-property": "1.0.0", - "extend-shallow": "2.0.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", "fragment-cache": "0.2.1", - "is-odd": "1.0.0", - "kind-of": "5.1.0", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", "object.pick": "1.3.0", - "regex-not": "1.0.0", + "regex-not": "1.0.2", "snapdragon": "0.8.1", - "to-regex": "3.0.1" + "to-regex": "3.0.2" }, "dependencies": { "arr-diff": { @@ -7071,9 +7245,9 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -7090,12 +7264,288 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "neo-async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz", + "integrity": "sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g==" + }, "netmask": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", "optional": true }, + "ng-packagr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-2.4.1.tgz", + "integrity": "sha512-cOKF59RzAftlLNgGGasTaczJ2GD7NZmA2GWmqXK7/BJFiRX2xMeYbbJke5dpYw7yUIkHJbZMd7WtcKSlVpJ4TQ==", + "requires": { + "@ngtools/json-schema": "1.1.0", + "autoprefixer": "7.2.6", + "browserslist": "2.11.3", + "chalk": "2.3.2", + "commander": "2.12.2", + "cpx": "1.5.0", + "fs-extra": "5.0.0", + "glob": "7.1.2", + "injection-js": "2.2.1", + "less": "2.7.3", + "node-sass": "4.7.2", + "node-sass-tilde-importer": "1.0.2", + "postcss": "6.0.19", + "postcss-clean": "1.1.0", + "postcss-url": "7.3.1", + "read-pkg-up": "3.0.0", + "rimraf": "2.6.2", + "rollup": "0.55.5", + "rollup-plugin-cleanup": "2.0.0", + "rollup-plugin-commonjs": "8.3.0", + "rollup-plugin-license": "0.6.0", + "rollup-plugin-node-resolve": "3.3.0", + "rxjs": "5.5.8", + "sorcery": "0.10.0", + "strip-bom": "3.0.0", + "stylus": "0.54.5", + "tar": "4.4.1", + "uglify-js": "3.3.16", + "update-notifier": "2.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "autoprefixer": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", + "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", + "requires": { + "browserslist": "2.11.3", + "caniuse-lite": "1.0.30000813", + "normalize-range": "0.1.2", + "num2fraction": "1.2.2", + "postcss": "6.0.19", + "postcss-value-parser": "3.3.0" + } + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "requires": { + "caniuse-lite": "1.0.30000813", + "electron-to-chromium": "1.3.37" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "requires": { + "errno": "0.1.4", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "minipass": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "requires": { + "find-up": "2.1.0", + "read-pkg": "3.0.0" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "rxjs": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.8.tgz", + "integrity": "sha512-Bz7qou7VAIoGiglJZbzbXa4vpX5BmTTN2Dj/se6+SwADtw4SihqBIiEa7VmTXJ8pynvq0iFr5Gx9VLyye1rIxQ==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "requires": { + "has-flag": "3.0.0" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "tar": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "uglify-js": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.16.tgz", + "integrity": "sha512-FMh5SRqJRGhv9BbaTffENIpDDQIoPDR8DBraunGORGhySArsXlw9++CN+BWzPBLpoI4RcSnpfGPnilTxWL3Vvg==", + "requires": { + "commander": "2.15.1", + "source-map": "0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } + }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -7105,9 +7555,9 @@ } }, "node-forge": { - "version": "0.6.33", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.33.tgz", - "integrity": "sha1-RjgRh59XPUUVWtap9D3ClujoXrw=" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" }, "node-gyp": { "version": "3.6.2", @@ -7169,7 +7619,7 @@ "stream-browserify": "2.0.1", "stream-http": "2.7.2", "string_decoder": "1.0.3", - "timers-browserify": "2.0.4", + "timers-browserify": "2.0.6", "tty-browserify": "0.0.0", "url": "0.11.0", "util": "0.10.3", @@ -7187,126 +7637,45 @@ } }, "timers-browserify": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", - "integrity": "sha512-uZYhyU3EX8O7HQP+J9fTVYwsq90Vr68xPEFo7yrVImIxYvHgukBEgOB/SgGoorWVTzGM/3Z+wUNnboA4M8jWrg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.6.tgz", + "integrity": "sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==", "requires": { "setimmediate": "1.0.5" } } } }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "optional": true, + "node-sass": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", + "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", + "async-foreach": "0.1.3", + "chalk": "1.1.3", + "cross-spawn": "3.0.1", + "gaze": "1.1.2", + "get-stdin": "4.0.1", + "glob": "7.1.2", + "in-publish": "2.0.0", + "lodash.assign": "4.2.0", + "lodash.clonedeep": "4.5.0", + "lodash.mergewith": "4.6.0", + "meow": "3.7.0", "mkdirp": "0.5.1", - "nopt": "4.0.1", + "nan": "2.8.0", + "node-gyp": "3.6.2", "npmlog": "4.1.2", - "rc": "1.2.3", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.4.1", - "tar": "2.2.1", - "tar-pack": "3.4.1" + "request": "2.79.0", + "sass-graph": "2.2.4", + "stdout-stream": "1.4.0", + "true-case-path": "1.0.2" }, "dependencies": { - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "optional": true, - "requires": { - "abbrev": "1.0.9", - "osenv": "0.1.4" - } - }, - "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "optional": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - } - } - }, - "node-sass": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", - "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==", - "requires": { - "async-foreach": "0.1.3", - "chalk": "1.1.3", - "cross-spawn": "3.0.1", - "gaze": "1.1.2", - "get-stdin": "4.0.1", - "glob": "7.1.2", - "in-publish": "2.0.0", - "lodash.assign": "4.2.0", - "lodash.clonedeep": "4.5.0", - "lodash.mergewith": "4.6.0", - "meow": "3.7.0", - "mkdirp": "0.5.1", - "nan": "2.8.0", - "node-gyp": "3.6.2", - "npmlog": "4.1.2", - "request": "2.79.0", - "sass-graph": "2.2.4", - "stdout-stream": "1.4.0", - "true-case-path": "1.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "caseless": { "version": "0.11.0", @@ -7380,6 +7749,14 @@ } } }, + "node-sass-tilde-importer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-sass-tilde-importer/-/node-sass-tilde-importer-1.0.2.tgz", + "integrity": "sha512-Swcmr38Y7uB78itQeBm3mThjxBy9/Ah/ykPIaURY/L6Nec9AyRoL/jJ7ECfMR+oZeCTVQNxVMu/aHU+TLRVbdg==", + "requires": { + "find-parent-dir": "0.3.0" + } + }, "nodemailer": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.7.2.tgz", @@ -7495,17 +7872,6 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "requires": { - "object-assign": "4.1.1", - "prepend-http": "1.0.4", - "query-string": "4.3.4", - "sort-keys": "1.1.2" - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -7581,6 +7947,22 @@ "is-descriptor": "0.1.6" } }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + } + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -7620,6 +8002,15 @@ } } }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.10.0" + } + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -7645,9 +8036,9 @@ } }, "obuf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", - "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, "on-finished": { "version": "2.3.0", @@ -7776,16 +8167,19 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-limit": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", - "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "requires": { + "p-try": "1.0.0" + } }, "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "requires": { - "p-limit": "1.1.0" + "p-limit": "1.2.0" } }, "p-map": { @@ -7793,6 +8187,11 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "pac-proxy-agent": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-1.1.0.tgz", @@ -7831,6 +8230,17 @@ } } }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "requires": { + "got": "6.7.1", + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0", + "semver": "5.4.1" + } + }, "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", @@ -7898,6 +8308,11 @@ "error-ex": "1.3.1" } }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -8053,6 +8468,11 @@ } } }, + "popper.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.1.tgz", + "integrity": "sha1-uIFeXNpvYvwgQuR2GGSfdYZuZ1M=" + }, "portfinder": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", @@ -8069,39 +8489,37 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "postcss": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz", - "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==", + "version": "6.0.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.19.tgz", + "integrity": "sha512-f13HRz0HtVwVaEuW6J6cOUCBLFtymhgyLPV7t4QEk2UD3twRI9IluDcQNdzQdBpiixkXj2OmzejhhTbSbDxNTg==", "requires": { - "chalk": "2.3.0", + "chalk": "2.3.2", "source-map": "0.6.1", - "supports-color": "5.1.0" + "supports-color": "5.3.0" }, "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", "requires": { - "ansi-styles": "3.2.0", + "ansi-styles": "3.2.1", "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - }, - "dependencies": { - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "requires": { - "has-flag": "2.0.0" - } - } + "supports-color": "5.3.0" } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "source-map": { "version": "0.6.1", @@ -8109,253 +8527,193 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", "requires": { - "has-flag": "2.0.0" + "has-flag": "3.0.0" } } } }, - "postcss-calc": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", - "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", + "postcss-clean": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-clean/-/postcss-clean-1.1.0.tgz", + "integrity": "sha512-83g3GqMbCM5NL6MlbbPLJ/m2NrUepBF44MoDk4Gt04QGXeXKh9+ilQa0DzLnYnvqYHQCw83nckuEzBFr2muwbg==", "requires": { - "postcss": "5.2.18", - "postcss-message-helpers": "2.0.0", - "reduce-css-calc": "1.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "clean-css": "4.1.11", + "postcss": "6.0.19" } }, - "postcss-colormin": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", - "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", + "postcss-import": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.1.0.tgz", + "integrity": "sha512-5l327iI75POonjxkXgdRCUS+AlzAdBx4pOvMEhTKTCjb1p8IEeVR9yx3cPbmN7LIWJLbfnIXxAhoB4jpD0c/Cw==", "requires": { - "colormin": "1.1.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" + "postcss": "6.0.19", + "postcss-value-parser": "3.3.0", + "read-cache": "1.0.0", + "resolve": "1.5.0" + } + }, + "postcss-load-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", + "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1", + "postcss-load-options": "1.2.0", + "postcss-load-plugins": "2.3.0" + } + }, + "postcss-load-options": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", + "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-load-plugins": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", + "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "requires": { + "cosmiconfig": "2.2.2", + "object-assign": "4.1.1" + } + }, + "postcss-loader": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.1.1.tgz", + "integrity": "sha512-f0J/DWE/hyO9/LH0WHpXkny/ZZ238sSaG3p1SRBtVZnFWUtD7GXIEgHoBg8cnAeRbmEvUxHQptY46zWfwNYj/w==", + "requires": { + "loader-utils": "1.1.0", + "postcss": "6.0.19", + "postcss-load-config": "1.2.0", + "schema-utils": "0.4.5" }, "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "ajv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz", + "integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=", "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" + "ajv": "6.2.0", + "ajv-keywords": "3.1.0" } } } }, - "postcss-convert-values": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", - "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", + "postcss-url": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.1.tgz", + "integrity": "sha512-Ya5KIjGptgz0OtrVYfi2UbLxVAZ6Emc4Of+Grx4Sf1deWlRpFwLr8FrtkUxfqh+XiZIVkXbjQrddE10ESpNmdA==", "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "mime": "1.6.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "postcss": "6.0.19", + "xxhashjs": "0.2.1" } }, - "postcss-discard-comments": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", - "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "renderkid": "2.0.1", + "utila": "0.4.0" } }, - "postcss-discard-duplicates": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", - "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "optional": true, "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "asap": "2.0.6" } }, - "postcss-discard-empty": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", - "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "protractor": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.1.tgz", + "integrity": "sha512-AW9qJ0prx2QEMy1gnhJ1Sl1WBQL2R3fx/VnG09FEmWprPIQPK14t0B83OB/pAGddpxiDCAAV0KiNNLf2c2Y/lQ==", "requires": { - "postcss": "5.2.18" + "@types/node": "6.0.104", + "@types/q": "0.0.32", + "@types/selenium-webdriver": "2.53.43", + "blocking-proxy": "1.0.1", + "chalk": "1.1.3", + "glob": "7.1.2", + "jasmine": "2.8.0", + "jasminewd2": "2.2.0", + "optimist": "0.6.1", + "q": "1.4.1", + "saucelabs": "1.3.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "0.4.18", + "webdriver-js-extender": "1.0.0", + "webdriver-manager": "12.0.6" }, "dependencies": { + "@types/node": { + "version": "6.0.104", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.104.tgz", + "integrity": "sha512-xPuI3Yeyc3u5SY7aFu6ILTJHFXo820DSfqNqYi1gxPmbpul+vLSfo3vhrY80d0+SdOYR9KdXHg6ozx4i/02LCg==" + }, + "adm-zip": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz", + "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=" + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -8371,2324 +8729,1824 @@ "has-ansi": "2.0.0", "strip-ansi": "3.0.1", "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } } }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" + "glob": "7.1.2" } - } - } - }, - "postcss-discard-overridden": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", - "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", - "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } + "source-map": "0.5.7" } }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "webdriver-manager": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", + "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", "requires": { + "adm-zip": "0.4.7", "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" + "del": "2.2.2", + "glob": "7.1.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "q": "1.4.1", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "xml2js": "0.4.19" } } } }, - "postcss-discard-unused": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", - "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", "requires": { - "postcss": "5.2.18", - "uniqs": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "forwarded": "0.1.2", + "ipaddr.js": "1.6.0" } }, - "postcss-filter-plugins": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", - "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "proxy-agent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-2.0.0.tgz", + "integrity": "sha1-V+tTR6qAXXTsaByyVknbo5yTNJk=", + "optional": true, "requires": { - "postcss": "5.2.18", - "uniqid": "4.1.1" + "agent-base": "2.1.1", + "debug": "2.6.9", + "extend": "3.0.1", + "http-proxy-agent": "1.0.0", + "https-proxy-agent": "1.0.0", + "lru-cache": "2.6.5", + "pac-proxy-agent": "1.1.0", + "socks-proxy-agent": "2.1.1" }, "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } + "lru-cache": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.6.5.tgz", + "integrity": "sha1-5W1jVBSO3o13B7WNFDIg/QjfD9U=", + "optional": true } } }, - "postcss-import": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-11.0.0.tgz", - "integrity": "sha1-qWLi34LTvFptpqOGhBdHIE9B71s=", - "requires": { - "postcss": "6.0.16", - "postcss-value-parser": "3.3.0", - "read-cache": "1.0.0", - "resolve": "1.1.7" - } + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" }, - "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1", - "postcss-load-options": "1.2.0", - "postcss-load-plugins": "2.3.0" - } + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1" + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.1.3", + "parse-asn1": "5.1.0", + "randombytes": "2.0.5" } }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "requires": { - "cosmiconfig": "2.2.2", - "object-assign": "4.1.1" + "end-of-stream": "1.4.1", + "once": "1.4.0" } }, - "postcss-loader": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-2.0.10.tgz", - "integrity": "sha512-xQaDcEgJ/2JqFY18zpFkik8vyYs7oS5ZRbrjvDqkP97k2wYWfPT4+qA0m4o3pTSCsz0u26PNqs8ZO9FRUWAqrA==", + "pumpify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", + "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", "requires": { - "loader-utils": "1.1.0", - "postcss": "6.0.16", - "postcss-load-config": "1.2.0", - "schema-utils": "0.3.0" + "duplexify": "3.5.4", + "inherits": "2.0.3", + "pump": "2.0.1" } }, - "postcss-merge-idents": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", - "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qjobs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.1.5.tgz", + "integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "optional": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "querystringify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz", + "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" + "is-number": "3.0.0", + "kind-of": "4.0.0" }, "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "kind-of": "3.2.2" }, "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" + "is-buffer": "1.1.6" } } } }, - "postcss-merge-longhand": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", - "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "randombytes": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "safe-buffer": "5.1.1" } }, - "postcss-merge-rules": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", - "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "randomfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", + "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", "requires": { - "browserslist": "1.7.7", - "caniuse-api": "1.6.1", - "postcss": "5.2.18", - "postcss-selector-parser": "2.2.3", - "vendors": "1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", - "requires": { - "caniuse-db": "1.0.30000780", - "electron-to-chromium": "1.3.28" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "randombytes": "2.0.5", + "safe-buffer": "5.1.1" } }, - "postcss-message-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", - "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, - "postcss-minify-font-values": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", - "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", "requires": { - "object-assign": "4.1.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" } }, - "postcss-minify-gradients": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", - "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=" + }, + "rc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", + "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" } }, - "postcss-minify-params": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", - "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", "requires": { - "alphanum-sort": "1.0.2", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0", - "uniqs": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "pify": "2.3.0" } }, - "postcss-minify-selectors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", - "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", + "dev": true, "requires": { - "alphanum-sort": "1.0.2", - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-selector-parser": "2.2.3" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "debuglog": "1.0.1", + "graceful-fs": "4.1.11", + "read-package-json": "2.0.12", + "readdir-scoped-modules": "1.0.2", + "semver": "5.4.1", + "slide": "1.1.6", + "util-extend": "1.0.3" } }, - "postcss-modules-extract-imports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", - "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", "requires": { - "postcss": "6.0.16" + "readable-stream": "2.3.3" } }, - "postcss-modules-local-by-default": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", - "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "read-package-json": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.12.tgz", + "integrity": "sha512-m7/I0+tP6D34EVvSlzCtuVA4D/dHL6OpLcn2e4XVP5X57pCKGUy1JjRSBVKHWpB+vUU91sL85h84qX0MdXzBSw==", + "dev": true, "requires": { - "css-selector-tokenizer": "0.7.0", - "postcss": "6.0.16" + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "json-parse-better-errors": "1.0.1", + "normalize-package-data": "2.4.0", + "slash": "1.0.0" } }, - "postcss-modules-scope": { + "read-pkg": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", - "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "requires": { - "css-selector-tokenizer": "0.7.0", - "postcss": "6.0.16" + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" } }, - "postcss-modules-values": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", - "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "requires": { - "icss-replace-symbols": "1.1.0", - "postcss": "6.0.16" + "find-up": "1.1.2", + "read-pkg": "1.1.0" } }, - "postcss-normalize-charset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", - "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" } }, - "postcss-normalize-url": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", - "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "readdir-scoped-modules": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz", + "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", + "dev": true, "requires": { - "is-absolute-url": "2.1.0", - "normalize-url": "1.9.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "debuglog": "1.0.1", + "dezalgo": "1.0.3", + "graceful-fs": "4.1.11", + "once": "1.4.0" } }, - "postcss-ordered-values": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", - "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" } }, - "postcss-reduce-idents": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", - "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "requires": { - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "indent-string": "2.1.0", + "strip-indent": "1.0.1" } }, - "postcss-reduce-initial": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", - "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "optional": true, "requires": { - "postcss": "5.2.18" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "double-ended-queue": "2.1.0-0", + "redis-commands": "1.3.1", + "redis-parser": "2.6.0" } }, - "postcss-reduce-transforms": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", - "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "redis-commands": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", + "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=", + "optional": true + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=", + "optional": true + }, + "reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==" + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" + }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "is-equal-shallow": "0.1.3" } }, - "postcss-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", - "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "requires": { - "flatten": "1.0.2", - "indexes-of": "1.0.1", - "uniq": "1.0.1" + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" } }, - "postcss-svgo": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", - "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", "requires": { - "is-svg": "2.1.0", - "postcss": "5.2.18", - "postcss-value-parser": "3.3.0", - "svgo": "0.7.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "regenerate": "1.3.3", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" } }, - "postcss-unique-selectors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", - "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", "requires": { - "alphanum-sort": "1.0.2", - "postcss": "5.2.18", - "uniqs": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "rc": "1.2.6", + "safe-buffer": "5.1.1" } }, - "postcss-url": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-7.3.0.tgz", - "integrity": "sha512-VBP6uf6iL3AZra23nkPkOEkS/5azj1xf/toRrjfkolfFEgg9Gyzg9UhJZeIsz12EGKZTNVeGbPa2XtaZm/iZvg==", + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", "requires": { - "mime": "1.6.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "postcss": "6.0.16", - "xxhashjs": "0.2.1" + "rc": "1.2.6" } }, - "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" }, - "postcss-zindex": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", - "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "requires": { - "has": "1.0.1", - "postcss": "5.2.18", - "uniqs": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "requires": { - "chalk": "1.1.3", - "js-base64": "2.4.0", - "source-map": "0.5.7", - "supports-color": "3.2.3" - } - } + "jsesc": "0.5.0" } }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", "requires": { - "renderkid": "2.0.1", - "utila": "0.4.0" + "css-select": "1.2.0", + "dom-converter": "0.1.4", + "htmlparser2": "3.3.0", + "strip-ansi": "3.0.1", + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" + } } }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "optional": true, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "requires": { - "asap": "2.0.6" + "is-finite": "1.0.2" } }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" - }, - "protractor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.0.tgz", - "integrity": "sha512-8z1TWtc/I9Kn4fkfg87DhkSAi0arul7DHBEeJ70sy66teQAeffjQED1s0Gduigme7hxHRYdYEKbhHYz28fpv5w==", + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", "requires": { - "@types/node": "6.0.101", - "@types/q": "0.0.32", - "@types/selenium-webdriver": "2.53.43", - "blocking-proxy": "1.0.1", - "chalk": "1.1.3", - "glob": "7.1.2", - "jasmine": "2.8.0", - "jasminewd2": "2.2.0", - "optimist": "0.6.1", - "q": "1.4.1", - "saucelabs": "1.3.0", - "selenium-webdriver": "3.6.0", - "source-map-support": "0.4.18", - "webdriver-js-extender": "1.0.0", - "webdriver-manager": "12.0.6" + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" }, "dependencies": { - "@types/node": { - "version": "6.0.101", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.101.tgz", - "integrity": "sha512-IQ7V3D6+kK1DArTqTBrnl3M+YgJZLw8ta8w3Q9xjR79HaJzMAoTbZ8TNzUTztrkCKPTqIstE2exdbs1FzsYLUw==" + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "adm-zip": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz", - "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=" + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.0" + } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.0" + } + } } }, - "q": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", - "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=" + "form-data": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "glob": "7.1.2" + "ajv": "5.5.2", + "har-schema": "2.0.0" } }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "requires": { - "source-map": "0.5.7" + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" }, - "webdriver-manager": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.0.6.tgz", - "integrity": "sha1-PfGkgZdwELTL+MnYXHpXeCjA5ws=", + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "adm-zip": "0.4.7", - "chalk": "1.1.3", - "del": "2.2.2", - "glob": "7.1.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "q": "1.4.1", - "request": "2.83.0", - "rimraf": "2.6.2", - "semver": "5.4.1", - "xml2js": "0.4.19" + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" } - } - } - }, - "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" - } - }, - "proxy-agent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-2.0.0.tgz", - "integrity": "sha1-V+tTR6qAXXTsaByyVknbo5yTNJk=", - "optional": true, + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "requestretry": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/requestretry/-/requestretry-1.12.2.tgz", + "integrity": "sha512-wDYnH4imurLs5upu31WoPaOFfEu31qhFlF7KgpYbBsmBagFmreZZo8E/XpoQ3erCP5za+72t8k8QI4wlrtwVXw==", + "optional": true, "requires": { - "agent-base": "2.1.1", - "debug": "2.6.9", "extend": "3.0.1", - "http-proxy-agent": "1.0.0", - "https-proxy-agent": "1.0.0", - "lru-cache": "2.6.5", - "pac-proxy-agent": "1.1.0", - "socks-proxy-agent": "2.1.1" - }, - "dependencies": { - "lru-cache": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.6.5.tgz", - "integrity": "sha1-5W1jVBSO3o13B7WNFDIg/QjfD9U=", - "optional": true - } + "lodash": "4.17.4", + "request": "2.83.0", + "when": "3.7.8" } }, - "prr": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", - "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=" + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" }, - "public-encrypt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", - "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.1.3", - "parse-asn1": "5.1.0", - "randombytes": "2.0.5" - } + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", "requires": { - "end-of-stream": "1.4.0", - "once": "1.4.0" + "path-parse": "1.0.5" } }, - "pumpify": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz", - "integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=", + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", "requires": { - "duplexify": "3.5.1", - "inherits": "2.0.3", - "pump": "1.0.3" + "resolve-from": "3.0.0" } }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, - "qjobs": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.1.5.tgz", - "integrity": "sha1-ZZ3p8s+NzCehSBJ28gU3cnI4LnM=" + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, - "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", - "optional": true + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", "requires": { - "object-assign": "4.1.1", - "strict-uri-encode": "1.1.0" + "hash-base": "2.0.2", + "inherits": "2.0.3" } }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "rollup": { + "version": "0.55.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.55.5.tgz", + "integrity": "sha512-2hke9NOy332kxvnmMQOgl7DHm94zihNyYJNd8ZLWo4U0EjFvjUkeWa0+ge+70bTg+mY0xJ7NUsf5kIhDtrGrtA==" }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + "rollup-plugin-cleanup": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-cleanup/-/rollup-plugin-cleanup-2.0.0.tgz", + "integrity": "sha1-hZdzGaO/VHUKnXX7kJx+UfWaLaQ=", + "requires": { + "acorn": "4.0.13", + "magic-string": "0.22.5", + "rollup-pluginutils": "2.0.1" + } }, - "querystringify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz", - "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=" + "rollup-plugin-commonjs": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.3.0.tgz", + "integrity": "sha512-PYs3OiYgENFYEmI3vOEm5nrp3eY90YZqd5vGmQqeXmhJsAWFIrFdROCvOasqJ1HgeTvqyYo9IGXnFDyoboNcgQ==", + "requires": { + "acorn": "5.5.3", + "estree-walker": "0.5.1", + "magic-string": "0.22.5", + "resolve": "1.5.0", + "rollup-pluginutils": "2.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" + }, + "estree-walker": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.1.tgz", + "integrity": "sha512-7HgCgz1axW7w5aOvgOQkoR1RMBkllygJrssU3BvymKQ95lxXYv6Pon17fBRDm9qhkvXZGijOULoSF9ShOk/ZLg==" + } + } }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "rollup-plugin-license": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-0.6.0.tgz", + "integrity": "sha512-Ttz65oRtNKcfV5icDkQTixc8ja64ueoXejRJoAtmjXYAWVg0qx+tu5rXmEOXWXmUXeGs0ARUVIAG0p1JK0gACQ==", "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "commenting": "1.0.5", + "lodash": "4.17.5", + "magic-string": "0.22.4", + "mkdirp": "0.5.1", + "moment": "2.21.0" }, "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "magic-string": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.4.tgz", + "integrity": "sha512-kxBL06p6iO2qPBHsqGK2b3cRwiRGpnmSuVWNhwHcMX7qJOUr1HvricYP1LZOCdkQBUp0jiWg2d6WJwR3vYgByw==", "requires": { - "is-buffer": "1.1.6" + "vlq": "0.2.3" } } } }, - "randombytes": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "rollup-plugin-node-resolve": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz", + "integrity": "sha512-9zHGr3oUJq6G+X0oRMYlzid9fXicBdiydhwGChdyeNRGPcN/majtegApRKHLR5drboUvEWU+QeUmGTyEZQs3WA==", "requires": { - "safe-buffer": "5.1.1" + "builtin-modules": "2.0.0", + "is-module": "1.0.0", + "resolve": "1.5.0" + }, + "dependencies": { + "builtin-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-2.0.0.tgz", + "integrity": "sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg==" + } } }, - "randomfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz", - "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==", + "rollup-pluginutils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz", + "integrity": "sha1-fslbNXP2VDpGpkYb2afFRFJdD8A=", "requires": { - "randombytes": "2.0.5", - "safe-buffer": "5.1.1" + "estree-walker": "0.3.1", + "micromatch": "2.3.11" } }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" + "aproba": "1.2.0" } }, - "raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=" - }, - "rc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.3.tgz", - "integrity": "sha1-UVdakA+N1oOBxxC0cSwhVMPiA1s=", - "optional": true, + "rxjs": { + "version": "6.0.0-tactical-rc.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.0.0-tactical-rc.1.tgz", + "integrity": "sha512-PbAYwhl4VH5U4YzgzA4O5pCbDV5eBN9zNtcb8QyHXFVql3pAm3l1f+uoEcXsLlemuYdFGCYPbU+CYq9KCaIbBA==", "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } } }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "requires": { - "pify": "2.3.0" + "ret": "0.1.15" } }, - "read-installed": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", - "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", - "dev": true, + "sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=", "requires": { - "debuglog": "1.0.1", + "es6-promise": "3.3.1", "graceful-fs": "4.1.11", - "read-package-json": "2.0.12", - "readdir-scoped-modules": "1.0.2", - "semver": "5.4.1", - "slide": "1.1.6", - "util-extend": "1.0.3" + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + }, + "dependencies": { + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + } } }, - "read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", "requires": { - "readable-stream": "2.3.3" + "glob": "7.1.2", + "lodash": "4.17.4", + "scss-tokenizer": "0.2.3", + "yargs": "7.1.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + } + } } }, - "read-package-json": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.12.tgz", - "integrity": "sha512-m7/I0+tP6D34EVvSlzCtuVA4D/dHL6OpLcn2e4XVP5X57pCKGUy1JjRSBVKHWpB+vUU91sL85h84qX0MdXzBSw==", - "dev": true, + "sass-loader": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz", + "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==", "requires": { - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "json-parse-better-errors": "1.0.1", - "normalize-package-data": "2.4.0", - "slash": "1.0.0" + "clone-deep": "2.0.2", + "loader-utils": "1.1.0", + "lodash.tail": "4.1.1", + "neo-async": "2.5.0", + "pify": "3.0.0" + }, + "dependencies": { + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "requires": { + "for-own": "1.0.0", + "is-plain-object": "2.0.4", + "kind-of": "6.0.2", + "shallow-clone": "1.0.0" + } + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "1.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "requires": { + "is-extendable": "0.1.1", + "kind-of": "5.1.0", + "mixin-object": "2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + } } }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "saucelabs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.3.0.tgz", + "integrity": "sha1-0kDoAJ33+ocwbsRXimm6O1xCT+4=", "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" + "https-proxy-agent": "1.0.0" } }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "schema-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", + "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "ajv": "5.5.2" } }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "js-base64": "2.4.0", + "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } + } } }, - "readdir-scoped-modules": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz", - "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", - "dev": true, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", "requires": { - "debuglog": "1.0.1", - "dezalgo": "1.0.3", - "graceful-fs": "4.1.11", - "once": "1.4.0" + "jszip": "3.1.5", + "rimraf": "2.6.2", + "tmp": "0.0.30", + "xml2js": "0.4.19" + }, + "dependencies": { + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "requires": { + "os-tmpdir": "1.0.2" + } + } } }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "selfsigned": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.2.tgz", + "integrity": "sha1-tESVgNmZKbZbEKSDiTAaZZIIh1g=", "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.3", - "set-immediate-shim": "1.0.1" + "node-forge": "0.7.1" } }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" + "semver": "5.4.1" } }, - "redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "optional": true, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", "requires": { - "double-ended-queue": "2.1.0-0", - "redis-commands": "1.3.1", - "redis-parser": "2.6.0" + "semver": "5.4.1" } }, - "redis-commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", - "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=", - "optional": true - }, - "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=", - "optional": true + "semver-intersect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.2.0.tgz", + "integrity": "sha512-XwQtuo56XVJd4JDV90L9RWjBluxWcnKDlQivIlF+Jvdhqgvk7KAroAqs8aJ/hjQW0wNPSSWDxhJLzYX+dwb13A==", + "requires": { + "semver": "5.4.1" + } }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "send": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", + "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", "requires": { - "balanced-match": "0.4.2", - "math-expression-evaluator": "1.2.17", - "reduce-function-call": "1.0.2" + "debug": "2.6.9", + "depd": "1.1.1", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" }, "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" } } }, - "reduce-function-call": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", - "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", + "serialize-javascript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.4.0.tgz", + "integrity": "sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=" + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", "requires": { - "balanced-match": "0.4.2" + "accepts": "1.3.5", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "1.0.3", + "http-errors": "1.6.2", + "mime-types": "2.1.17", + "parseurl": "1.3.2" }, "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + }, + "dependencies": { + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + } + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" } } }, - "reflect-metadata": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.10.tgz", - "integrity": "sha1-tPg3BEFqytiZiMmxVjXUfgO5NEo=" - }, - "regenerate": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", - "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" - }, - "regenerator-runtime": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", - "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", "requires": { - "is-equal-shallow": "0.1.3" + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.1" } }, - "regex-not": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.0.tgz", - "integrity": "sha1-Qvg+OXcWIt+CawKvF2Ul1qXxV/k=", - "requires": { - "extend-shallow": "2.0.1" - } + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "set-getter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", "requires": { - "regenerate": "1.3.3", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" + "to-object-path": "0.3.0" } }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "requires": { - "jsesc": "0.5.0" + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } } }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" }, - "renderkid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", - "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "sha.js": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", + "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", "requires": { - "css-select": "1.2.0", - "dom-converter": "0.1.4", - "htmlparser2": "3.3.0", - "strip-ansi": "3.0.1", - "utila": "0.3.3" + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "requires": { + "json-stable-stringify": "0.0.1", + "sha.js": "2.4.9" }, "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=" + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "requires": { + "jsonify": "0.0.0" + } } } }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "1.0.0" + } }, - "repeat-string": { + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shell-quote": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "silent-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", + "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", "requires": { - "is-finite": "1.0.2" + "debug": "2.6.9" } }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "slack-node": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/slack-node/-/slack-node-0.2.0.tgz", + "integrity": "sha1-3kuN3aqLeT9h29KTgQT9q/N9+jA=", + "optional": true, "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "requestretry": "1.12.2" + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "smart-buffer": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", + "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=" + }, + "smtp-connection": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz", + "integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=", + "requires": { + "httpntlm": "1.6.1", + "nodemailer-shared": "1.1.0" + } + }, + "snapdragon": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", + "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "2.0.2" }, "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "hoek": "4.2.0" + "is-extendable": "0.1.1" } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "boom": "5.2.0" + "kind-of": "3.2.2" }, "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "hoek": "4.2.0" + "is-buffer": "1.1.6" } } } }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" } }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "is-descriptor": "1.0.2" } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" } } }, - "requestretry": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/requestretry/-/requestretry-1.12.2.tgz", - "integrity": "sha512-wDYnH4imurLs5upu31WoPaOFfEu31qhFlF7KgpYbBsmBagFmreZZo8E/XpoQ3erCP5za+72t8k8QI4wlrtwVXw==", - "optional": true, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "requires": { - "extend": "3.0.1", - "lodash": "4.17.4", - "request": "2.83.0", - "when": "3.7.8" + "kind-of": "3.2.2" } }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "requires": { - "resolve-from": "3.0.0" + "hoek": "2.16.3" } }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "socket.io": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", + "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", "requires": { - "align-text": "0.1.4" + "debug": "2.6.9", + "engine.io": "3.1.4", + "socket.io-adapter": "1.1.1", + "socket.io-client": "2.0.4", + "socket.io-parser": "3.1.2" } }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" }, - "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "socket.io-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", + "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", "requires": { - "hash-base": "2.0.2", - "inherits": "2.0.3" + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.6.9", + "engine.io-client": "3.1.4", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.1.2", + "to-array": "0.1.4" } }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "socket.io-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", + "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", "requires": { - "aproba": "1.2.0" + "component-emitter": "1.2.1", + "debug": "2.6.9", + "has-binary2": "1.0.2", + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } } }, - "rxjs": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", - "integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==", + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", "requires": { - "symbol-observable": "1.0.1" + "faye-websocket": "0.10.0", + "uuid": "3.1.0" } }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", "requires": { - "glob": "7.1.2", - "lodash": "4.17.4", - "scss-tokenizer": "0.2.3", - "yargs": "7.1.0" + "debug": "2.6.9", + "eventsource": "0.1.6", + "faye-websocket": "0.11.1", + "inherits": "2.0.3", + "json3": "3.3.2", + "url-parse": "1.2.0" }, "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - } - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", "requires": { - "camelcase": "3.0.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "1.4.0", - "read-pkg-up": "1.0.1", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "1.0.2", - "which-module": "1.0.0", - "y18n": "3.2.1", - "yargs-parser": "5.0.0" + "websocket-driver": "0.7.0" } } } }, - "sass-loader": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.6.tgz", - "integrity": "sha512-c3/Zc+iW+qqDip6kXPYLEgsAu2lf4xz0EZDplB7EmSUMda12U1sGJPetH55B/j9eu0bTtKzKlNPWWyYC7wFNyQ==", + "socks": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz", + "integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=", "requires": { - "async": "2.6.0", - "clone-deep": "0.3.0", - "loader-utils": "1.1.0", - "lodash.tail": "4.1.1", - "pify": "3.0.0" + "ip": "1.1.5", + "smart-buffer": "1.1.15" }, "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "requires": { - "lodash": "4.17.4" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" } } }, - "saucelabs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.3.0.tgz", - "integrity": "sha1-0kDoAJ33+ocwbsRXimm6O1xCT+4=", + "socks-proxy-agent": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz", + "integrity": "sha512-sFtmYqdUK5dAMh85H0LEVFUCO7OhJJe1/z2x/Z6mxp3s7/QPf1RkZmpZy+BpuU0bEjcV9npqKjq9Y3kwFUjnxw==", "requires": { - "https-proxy-agent": "1.0.0" + "agent-base": "2.1.1", + "extend": "3.0.1", + "socks": "1.1.10" } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "sorcery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", + "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=", + "requires": { + "buffer-crc32": "0.2.13", + "minimist": "1.2.0", + "sander": "0.5.1", + "sourcemap-codec": "1.4.1" + } }, - "schema-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", - "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", "requires": { - "ajv": "5.5.2" + "atob": "2.0.3", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" } }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "source-map-support": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.0.tgz", + "integrity": "sha512-vUoN3I7fHQe0R/SJLKRdKYuEdRGogsviXFkHHo17AWaTGv17VLnxw+CFXvqy+y4ORZ3doWLQcxRYfwKrsd/H7Q==", "requires": { - "js-base64": "2.4.0", - "source-map": "0.4.4" + "source-map": "0.6.1" }, "dependencies": { "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": "1.0.1" - } + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, - "selenium-webdriver": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", - "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "sourcemap-codec": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.1.tgz", + "integrity": "sha512-hX1eNBNuilj8yfFnECh0DzLgwKpBLMIvmhgEhixXNui8lMLBInTI8Kyxt++RwJnMNu7cAUo635L2+N1TxMJCzA==" + }, + "spdx": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/spdx/-/spdx-0.5.1.tgz", + "integrity": "sha1-02wnUIi0jXWpBGzUSoOM5LUzmZg=", + "dev": true, "requires": { - "jszip": "3.1.5", - "rimraf": "2.6.2", - "tmp": "0.0.30", - "xml2js": "0.4.19" - }, - "dependencies": { - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, - "tmp": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", - "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", - "requires": { - "os-tmpdir": "1.0.2" - } - } + "spdx-exceptions": "1.0.5", + "spdx-license-ids": "1.2.2" } }, - "selfsigned": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.1.tgz", - "integrity": "sha1-v4y3uDJWxFUeMTR8YxF3jbme7FI=", + "spdx-compare": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-0.1.2.tgz", + "integrity": "sha1-sGrz6jSvdDfZGp9Enq8tLpPDyPs=", + "dev": true, "requires": { - "node-forge": "0.6.33" + "spdx-expression-parse": "1.0.4", + "spdx-ranges": "1.0.1" } }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "semver-dsl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", - "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", "requires": { - "semver": "5.4.1" + "spdx-license-ids": "1.2.2" } }, - "semver-intersect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.2.0.tgz", - "integrity": "sha512-XwQtuo56XVJd4JDV90L9RWjBluxWcnKDlQivIlF+Jvdhqgvk7KAroAqs8aJ/hjQW0wNPSSWDxhJLzYX+dwb13A==", + "spdx-exceptions": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.5.tgz", + "integrity": "sha1-nSGsTaS9tx0GD7dOWmdTHQMsu6Y=", + "dev": true + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "spdx-ranges": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-1.0.1.tgz", + "integrity": "sha1-D07se46kjtIC43S7iULo0Y3AET4=", + "dev": true + }, + "spdx-satisfies": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-0.1.3.tgz", + "integrity": "sha1-Z6HydOYRXUquKK/kdNt2FkvhC9w=", + "dev": true, "requires": { - "semver": "5.4.1" + "spdx-compare": "0.1.2", + "spdx-expression-parse": "1.0.4" } }, - "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", "requires": { "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" - }, - "dependencies": { - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - } + "handle-thing": "1.2.5", + "http-deceiver": "1.2.7", + "safe-buffer": "5.1.1", + "select-hose": "2.0.0", + "spdy-transport": "2.1.0" } }, - "serialize-javascript": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.4.0.tgz", - "integrity": "sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=" - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "spdy-transport": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", + "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==", "requires": { - "accepts": "1.3.4", - "batch": "0.6.1", "debug": "2.6.9", - "escape-html": "1.0.3", - "http-errors": "1.6.2", - "mime-types": "2.1.17", - "parseurl": "1.3.2" - }, - "dependencies": { - "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.6.1" - } - } + "detect-node": "2.0.3", + "hpack.js": "2.1.6", + "obuf": "1.1.2", + "readable-stream": "2.3.3", + "safe-buffer": "5.1.1", + "wbuf": "1.7.3" } }, - "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.1" + "through": "2.3.8" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-getter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "requires": { - "to-object-path": "0.3.0" + "extend-shallow": "3.0.2" } }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "through2": "2.0.3" } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "setprototypeof": { + "sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "sha.js": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", - "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "ssri": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.2.4.tgz", + "integrity": "sha512-UnEAgMZa15973iH7cUi0AHjJn1ACDIkaMyZILoqwN6yzt+4P81I8tBc5Hl+qwi5auMplZtPQsHrPBR5vJLcQtQ==", "requires": { - "inherits": "2.0.3", "safe-buffer": "5.1.1" } }, - "shallow-clone": { + "static-extend": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", - "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "requires": { - "is-extendable": "0.1.1", - "kind-of": "2.0.1", - "lazy-cache": "0.2.7", - "mixin-object": "2.0.1" + "define-property": "0.2.5", + "object-copy": "0.1.0" }, "dependencies": { - "kind-of": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", - "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-buffer": "1.1.6" + "is-descriptor": "0.1.6" } }, - "lazy-cache": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", - "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=" - } - } - }, - "shasum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", - "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", - "requires": { - "json-stable-stringify": "0.0.1", - "sha.js": "2.4.9" - }, - "dependencies": { - "json-stable-stringify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", - "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "jsonify": "0.0.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "requires": { - "array-filter": "0.0.1", - "array-map": "0.0.0", - "array-reduce": "0.0.0", - "jsonify": "0.0.0" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "silent-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/silent-error/-/silent-error-1.1.0.tgz", - "integrity": "sha1-IglwbxyFCp8dENDYQJGLRvJuG8k=", - "requires": { - "debug": "2.6.9" - } - }, - "slack-node": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/slack-node/-/slack-node-0.2.0.tgz", - "integrity": "sha1-3kuN3aqLeT9h29KTgQT9q/N9+jA=", - "optional": true, - "requires": { - "requestretry": "1.12.2" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, - "smart-buffer": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", - "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=" - }, - "smtp-connection": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz", - "integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=", - "requires": { - "httpntlm": "1.6.1", - "nodemailer-shared": "1.1.0" - } - }, - "snapdragon": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", - "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", - "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.1", - "use": "2.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { - "is-descriptor": "0.1.6" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, "is-descriptor": { @@ -10708,1443 +10566,1474 @@ } } }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "stats-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/stats-webpack-plugin/-/stats-webpack-plugin-0.6.2.tgz", + "integrity": "sha1-LFlJtTHgf4eojm6k3PrFOqjHWis=", "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "lodash": "4.17.4" } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "3.2.2" - } + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "stdout-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", + "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", "requires": { - "hoek": "2.16.3" + "readable-stream": "2.3.3" } }, - "socket.io": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", - "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", "requires": { - "debug": "2.6.9", - "engine.io": "3.1.4", - "socket.io-adapter": "1.1.1", - "socket.io-client": "2.0.4", - "socket.io-parser": "3.1.2" + "inherits": "2.0.3", + "readable-stream": "2.3.3" } }, - "socket.io-adapter": { + "stream-combiner2": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "requires": { + "duplexer2": "0.1.4", + "readable-stream": "2.3.3" + } }, - "socket.io-client": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", - "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "2.6.9", - "engine.io-client": "3.1.4", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "3.1.2", - "to-array": "0.1.4" + "end-of-stream": "1.4.1", + "stream-shift": "1.0.0" } }, - "socket.io-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz", - "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=", + "stream-http": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", "requires": { - "component-emitter": "1.2.1", - "debug": "2.6.9", - "has-binary2": "1.0.2", - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" } }, - "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", "requires": { - "faye-websocket": "0.10.0", - "uuid": "3.1.0" + "inherits": "2.0.3", + "readable-stream": "2.3.3" } }, - "sockjs-client": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", - "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "streamroller": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.6.0.tgz", + "integrity": "sha1-CV17BsfMUlg1ytLlmPdrseDuaTo=", "requires": { + "date-format": "1.2.0", "debug": "2.6.9", - "eventsource": "0.1.6", - "faye-websocket": "0.11.1", - "inherits": "2.0.3", - "json3": "3.3.2", - "url-parse": "1.2.0" - }, - "dependencies": { - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "requires": { - "websocket-driver": "0.7.0" - } - } + "mkdirp": "0.5.1", + "readable-stream": "2.3.3" } }, - "socks": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz", - "integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=", + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "ip": "1.1.5", - "smart-buffer": "1.1.15" - }, - "dependencies": { - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - } + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, - "socks-proxy-agent": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz", - "integrity": "sha512-sFtmYqdUK5dAMh85H0LEVFUCO7OhJJe1/z2x/Z6mxp3s7/QPf1RkZmpZy+BpuU0bEjcV9npqKjq9Y3kwFUjnxw==", + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "requires": { - "agent-base": "2.1.1", - "extend": "3.0.1", - "socks": "1.1.10" + "safe-buffer": "5.1.1" } }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "is-plain-obj": "1.1.0" + "ansi-regex": "2.1.1" } }, - "source-list-map": { + "strip-bom": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, - "source-map-loader": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.3.tgz", - "integrity": "sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==", + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "requires": { - "async": "2.6.0", - "loader-utils": "0.2.17", - "source-map": "0.6.1" + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "style-loader": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.20.2.tgz", + "integrity": "sha512-FrLMGaOLVhS5pvoez3eJyc0ktchT1inEZziBSjBq1hHQBK3GFkF57Qd825DcrUhjaAWQk70MKrIl5bfjadR/Dg==", + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.4.5" }, "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "ajv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz", + "integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=", "requires": { - "lodash": "4.17.4" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "requires": { - "big.js": "3.2.0", - "emojis-list": "2.1.0", - "json5": "0.5.1", - "object-assign": "4.1.1" + "ajv": "6.2.0", + "ajv-keywords": "3.1.0" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", - "requires": { - "atob": "2.0.3", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" - } - }, - "source-map-support": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.0.tgz", - "integrity": "sha512-vUoN3I7fHQe0R/SJLKRdKYuEdRGogsviXFkHHo17AWaTGv17VLnxw+CFXvqy+y4ORZ3doWLQcxRYfwKrsd/H7Q==", + "stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", "requires": { - "source-map": "0.6.1" + "css-parse": "1.7.0", + "debug": "2.6.9", + "glob": "7.0.6", + "mkdirp": "0.5.1", + "sax": "0.5.8", + "source-map": "0.1.43" }, "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": "1.0.1" + } } } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/spdx/-/spdx-0.5.1.tgz", - "integrity": "sha1-02wnUIi0jXWpBGzUSoOM5LUzmZg=", - "dev": true, + "stylus-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", + "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", "requires": { - "spdx-exceptions": "1.0.5", - "spdx-license-ids": "1.2.2" + "loader-utils": "1.1.0", + "lodash.clonedeep": "4.5.0", + "when": "3.6.4" + }, + "dependencies": { + "when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=" + } } }, - "spdx-compare": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-0.1.2.tgz", - "integrity": "sha1-sGrz6jSvdDfZGp9Enq8tLpPDyPs=", - "dev": true, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", "requires": { - "spdx-expression-parse": "1.0.4", - "spdx-ranges": "1.0.1" + "minimist": "1.2.0" } }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "requires": { - "spdx-license-ids": "1.2.2" + "has-flag": "1.0.0" } }, - "spdx-exceptions": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.5.tgz", - "integrity": "sha1-nSGsTaS9tx0GD7dOWmdTHQMsu6Y=", - "dev": true - }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" - }, - "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" - }, - "spdx-ranges": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-1.0.1.tgz", - "integrity": "sha1-D07se46kjtIC43S7iULo0Y3AET4=", - "dev": true + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, - "spdx-satisfies": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-0.1.3.tgz", - "integrity": "sha1-Z6HydOYRXUquKK/kdNt2FkvhC9w=", - "dev": true, + "syntax-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", + "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", "requires": { - "spdx-compare": "0.1.2", - "spdx-expression-parse": "1.0.4" + "acorn": "4.0.13" } }, - "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", - "requires": { - "debug": "2.6.9", - "handle-thing": "1.2.5", - "http-deceiver": "1.2.7", - "safe-buffer": "5.1.1", - "select-hose": "2.0.0", - "spdy-transport": "2.0.20" - } + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==" }, - "spdy-transport": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz", - "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=", - "requires": { - "debug": "2.6.9", - "detect-node": "2.0.3", - "hpack.js": "2.1.6", - "obuf": "1.1.1", - "readable-stream": "2.3.3", - "safe-buffer": "5.1.1", - "wbuf": "1.7.2" - } - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "requires": { - "through": "2.3.8" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "tar": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz", + "integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==", "requires": { - "extend-shallow": "3.0.2" + "chownr": "1.0.1", + "minipass": "2.2.1", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "yallist": "3.0.2" }, "dependencies": { - "extend-shallow": { + "yallist": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "temp": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", + "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", "requires": { - "through2": "2.0.3" + "os-tmpdir": "1.0.2", + "rimraf": "2.2.8" } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } + "execa": "0.7.0" } }, - "ssri": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.0.0.tgz", - "integrity": "sha512-728D4yoQcQm1ooZvSbywLkV1RjfITZXh0oWrhM/lnsx3nAHx7LsRGJWB/YyvoceAYRq98xqbstiN4JBv1/wNHg==", - "requires": { - "safe-buffer": "5.1.1" - } + "text-extensions": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", + "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==" }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } + "readable-stream": "2.3.3", + "xtend": "4.0.1" } }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", + "optional": true }, - "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "thunky": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", + "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=" + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", "requires": { - "readable-stream": "2.3.3" + "process": "0.11.10" } }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "timespan": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz", + "integrity": "sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk=", + "optional": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3" + "os-tmpdir": "1.0.2" } }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "requires": { - "duplexer2": "0.1.4", - "readable-stream": "2.3.3" + "kind-of": "3.2.2" } }, - "stream-each": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", - "integrity": "sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==", + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "requires": { - "end-of-stream": "1.4.0", - "stream-shift": "1.0.0" + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" } }, - "stream-http": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "requires": { - "builtin-status-codes": "3.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "to-arraybuffer": "1.0.1", - "xtend": "4.0.1" + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + } + } } }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "toposort": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.6.tgz", + "integrity": "sha1-wxdI5V0hDv/AD9zcfW5o19e7nOw=" }, - "stream-splicer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", - "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3" + "punycode": "1.4.1" } }, - "streamroller": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.6.0.tgz", - "integrity": "sha1-CV17BsfMUlg1ytLlmPdrseDuaTo=", - "requires": { - "date-format": "1.2.0", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "readable-stream": "2.3.3" - } - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==" }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "0.2.1" - } + "treeify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.0.1.tgz", + "integrity": "sha1-abPNAiAioWhCTnz6HO1EyTnT6y8=", + "dev": true }, - "strip-eof": { + "trim-newlines": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, - "strip-indent": { + "trim-off-newlines": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "optional": true + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" }, - "style-loader": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.19.1.tgz", - "integrity": "sha512-IRE+ijgojrygQi3rsqT0U4dd+UcPCqcVvauZpCnQrGAlEe+FUIyrK93bUDScamesjP08JlQNsFJU+KmPedP5Og==", - "requires": { - "loader-utils": "1.1.0", - "schema-utils": "0.3.0" - } + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, - "stylus": { - "version": "0.54.5", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", - "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "true-case-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", + "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", "requires": { - "css-parse": "1.7.0", - "debug": "2.6.9", - "glob": "7.0.6", - "mkdirp": "0.5.1", - "sax": "0.5.8", - "source-map": "0.1.43" + "glob": "6.0.4" }, "dependencies": { "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", "requires": { - "fs.realpath": "1.0.0", "inflight": "1.0.6", "inherits": "2.0.3", "minimatch": "3.0.4", "once": "1.4.0", "path-is-absolute": "1.0.1" } + } + } + }, + "ts-node": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.0.tgz", + "integrity": "sha512-mlSim/sQS1s5iT3KZEKXRaqsGC7xM2QoxkrhfznZJyou18dl47PTnY7/KMmbGqiVoQrO9Hk53CYpcychF5TNrQ==", + "requires": { + "arrify": "1.0.1", + "chalk": "2.3.1", + "diff": "3.4.0", + "make-error": "1.3.2", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.5.3", + "yn": "2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + } }, - "sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz", + "integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==", "requires": { - "amdefine": "1.0.1" + "source-map": "0.6.1" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "requires": { + "has-flag": "3.0.0" } } } }, - "stylus-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.1.tgz", - "integrity": "sha1-d/SzT9Aw0lsmF7z1UT21sHMMQIk=", + "tsickle": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.27.5.tgz", + "integrity": "sha512-NP+CjM1EXza/M8mOXBLH3vkFEJiu1zfEAlC5WdJxHPn8l96QPz5eooP6uAgYtw1CcKfuSyIiheNUdKxtDWCNeg==", "requires": { - "loader-utils": "1.1.0", - "lodash.clonedeep": "4.5.0", - "when": "3.6.4" + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map": "0.6.1", + "source-map-support": "0.5.0" }, "dependencies": { - "when": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", - "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "requires": { - "minimist": "1.2.0" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "1.0.0" - } + "tslib": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz", + "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=" }, - "svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", "requires": { - "coa": "1.0.4", - "colors": "1.1.2", - "csso": "2.3.2", - "js-yaml": "3.7.0", - "mkdirp": "0.5.1", - "sax": "1.2.4", - "whet.extend": "0.9.9" + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.0", + "commander": "2.12.2", + "diff": "3.4.0", + "glob": "7.1.2", + "js-yaml": "3.10.0", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.4.1", + "tslib": "1.8.1", + "tsutils": "2.22.2" }, "dependencies": { - "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "requires": { + "path-parse": "1.0.5" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", "requires": { - "argparse": "1.0.9", - "esprima": "2.7.3" + "has-flag": "2.0.0" } } } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + "tsscmp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", + "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=", + "optional": true }, - "syntax-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz", - "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=", + "tsutils": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.22.2.tgz", + "integrity": "sha512-u06FUSulCJ+Y8a2ftuqZN6kIGqdP2yJjUPEngXqmdPND4UQfb04igcotH+dw+IFr417yP6muCLE8/5/Qlfnx0w==", "requires": { - "acorn": "4.0.13" + "tslib": "1.8.1" } }, - "tapable": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", - "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" }, - "tar": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz", - "integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==", + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "chownr": "1.0.1", - "minipass": "2.2.1", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "yallist": "3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - } + "safe-buffer": "5.1.1" } }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "optional": true, - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - }, - "dependencies": { - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "optional": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - } - } + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "requires": { - "os-tmpdir": "1.0.2", - "rimraf": "2.2.8" + "prelude-ls": "1.1.2" } }, - "text-extensions": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", - "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", "requires": { - "readable-stream": "2.3.3", - "xtend": "4.0.1" + "media-typer": "0.3.0", + "mime-types": "2.1.17" } }, - "thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", - "optional": true - }, - "thunky": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz", - "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=" + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "time-stamp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.0.tgz", - "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=" + "typescript": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", + "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==" }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", "requires": { - "process": "0.11.10" + "commander": "2.13.0", + "source-map": "0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, - "timespan": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz", - "integrity": "sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk=", - "optional": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "optional": true, "requires": { - "os-tmpdir": "1.0.2" + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" } }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "3.2.2" - } + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true }, - "to-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.1.tgz", - "integrity": "sha1-FTWL7kosg712N3uh3ASdDxiDeq4=", + "uglifyjs-webpack-plugin": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.2.tgz", + "integrity": "sha512-CG/NvzXfemUAm5Y4Guh5eEaJYHtkG7kKNpXEJHp9QpxsFVB5/qKvYWoMaq4sa99ccZ0hM3MK8vQV9XPZB4357A==", "requires": { - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "regex-not": "1.0.0" + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.4.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.5.4" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "ajv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.0.tgz", + "integrity": "sha1-r6wpW7qgFSRJ5SJ0LkVHwa6TKNI=", "requires": { - "is-descriptor": "0.1.6" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "ajv": "6.2.0", + "ajv-keywords": "3.1.0" } }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "umd": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", + "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=" + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" }, "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "kind-of": "3.2.2" + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" } } } }, - "toposort": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.6.tgz", - "integrity": "sha1-wxdI5V0hDv/AD9zcfW5o19e7nOw=" - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", "requires": { - "punycode": "1.4.1" + "unique-slug": "2.0.0" } }, - "tree-kill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", - "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==" - }, - "treeify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.0.1.tgz", - "integrity": "sha1-abPNAiAioWhCTnz6HO1EyTnT6y8=", - "dev": true + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "requires": { + "imurmurhash": "0.1.4" + } }, - "trim-newlines": { + "unique-string": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "1.0.0" + } }, - "trim-off-newlines": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, - "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "requires": { - "glob": "6.0.4" + "has-value": "0.3.1", + "isobject": "3.0.1" }, "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" } } }, - "ts-node": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.0.tgz", - "integrity": "sha512-mlSim/sQS1s5iT3KZEKXRaqsGC7xM2QoxkrhfznZJyou18dl47PTnY7/KMmbGqiVoQrO9Hk53CYpcychF5TNrQ==", + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" + }, + "upath": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.0.4.tgz", + "integrity": "sha512-d4SJySNBXDaQp+DPrziv3xGS6w3d2Xt69FijJr86zMPBy23JEloMCEOUBBzuN7xCtjLCnmB9tI/z7SBCahHBOw==" + }, + "update-notifier": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.4.0.tgz", + "integrity": "sha1-+bTHAPv9TsEsgRWHJYd31WPYyGY=", "requires": { - "arrify": "1.0.1", - "chalk": "2.3.1", - "diff": "3.4.0", - "make-error": "1.3.2", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "source-map-support": "0.5.3", - "yn": "2.0.0" + "boxen": "1.3.0", + "chalk": "2.2.2", + "configstore": "3.1.2", + "import-lazy": "2.1.0", + "is-ci": "1.1.0", + "is-installed-globally": "0.1.0", + "is-npm": "1.0.0", + "latest-version": "3.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "3.0.0" + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + }, + "uri-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-3.0.2.tgz", + "integrity": "sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=", + "requires": { + "punycode": "2.1.0" }, "dependencies": { - "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz", - "integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==", - "requires": { - "source-map": "0.6.1" - } - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "requires": { - "has-flag": "3.0.0" - } + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" } } }, - "tsickle": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.26.0.tgz", - "integrity": "sha512-eWJ2CUfttGK0LqF9iJ/Avnxbj4M+fCyJ50Zag3wm73Fut1hsasPRHKxKdrMWVj4BMHnQNx7TO+DdNmLmJTSuNw==", + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "requires": { - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "source-map": "0.5.7", - "source-map-support": "0.4.18" + "punycode": "1.3.2", + "querystring": "0.2.0" }, "dependencies": { - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "requires": { - "source-map": "0.5.7" - } + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, - "tslib": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz", - "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=" + "url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=" }, - "tslint": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", - "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "url-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.0.1.tgz", + "integrity": "sha512-rAonpHy7231fmweBKUFe0bYnlGDty77E+fm53NZdij7j/YOpyGzc7ttqG1nAXl3aRs0k41o0PC3TvGXQiw2Zvw==", "requires": { - "babel-code-frame": "6.26.0", - "builtin-modules": "1.1.1", - "chalk": "2.3.0", - "commander": "2.12.2", - "diff": "3.4.0", - "glob": "7.1.2", - "js-yaml": "3.10.0", - "minimatch": "3.0.4", - "resolve": "1.5.0", - "semver": "5.4.1", - "tslib": "1.8.1", - "tsutils": "2.16.0" + "loader-utils": "1.1.0", + "mime": "2.2.0", + "schema-utils": "0.4.5" }, "dependencies": { - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "ajv": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.1.tgz", + "integrity": "sha1-KKarxJOiq+D7TIUHrK7bQ/pVBnE=", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "requires": { - "path-parse": "1.0.5" - } + "mime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", + "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==" }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "requires": { - "has-flag": "2.0.0" + "ajv": "6.2.1", + "ajv-keywords": "3.1.0" } } } }, - "tsscmp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", - "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=", - "optional": true - }, - "tsutils": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.16.0.tgz", - "integrity": "sha512-9Ier/60O7OZRNPiw+or5QAtAY4kQA+WDiO/r6xOYATEyefH9bdfvTRLCxrYnFhQlZfET2vYXKfpr3Vw2BiArZw==", + "url-parse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", + "integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==", "requires": { - "tslib": "1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "1.1.2" - } - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" + "querystringify": "1.0.0", + "requires-port": "1.0.0" + }, + "dependencies": { + "querystringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", + "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=" + } } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typescript": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", - "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "prepend-http": "1.0.4" } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, - "uglifyjs-webpack-plugin": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.8.tgz", - "integrity": "sha512-XG8/QmR1pyPeE1kj2aigo5kos8umefB31zW+PMvAAytHSB0T/vQvN6sqt8+Sh+y0b0A7zlmxNi2dzRnj0wcqGA==", + "use": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", + "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", "requires": { - "cacache": "10.0.1", - "find-cache-dir": "1.0.0", - "schema-utils": "0.4.2", - "serialize-javascript": "1.4.0", - "source-map": "0.6.1", - "uglify-es": "3.3.8", - "webpack-sources": "1.1.0", - "worker-farm": "1.5.2" + "define-property": "0.2.5", + "isobject": "3.0.1", + "lazy-cache": "2.0.2" }, "dependencies": { - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" + "is-descriptor": "0.1.6" } }, - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "schema-utils": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.2.tgz", - "integrity": "sha512-LCuuUj7L43TbSIqeERp+/Z2FH/NxSA48mqcWlGTSYUUKsevGafj2SpyaFVTxyWWFLkIAS3p7jDTLpNsrU7PXoA==", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "chalk": "2.3.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "requires": { - "has-flag": "2.0.0" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" } }, - "uglify-es": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.8.tgz", - "integrity": "sha512-j8li0jWcAN6yBuAVYFZEFyYINZAm4WEdMwkA6qXFi4TLrze3Mp0Le7QjW6LR9HQjQJ2zRa9VgnFLs3PatijWOw==", + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + }, + "lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", "requires": { - "commander": "2.13.0", - "source-map": "0.6.1" + "set-getter": "0.1.0" } } } }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "optional": true - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, - "umd": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", - "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=" - }, - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "useragent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", + "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" + "lru-cache": "2.2.4", + "tmp": "0.0.33" }, "dependencies": { - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" - } + "lru-cache": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=" } } }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "uniqid": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", - "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "requires": { - "macaddress": "0.2.8" + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + } } }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "unique-filename": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", - "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", - "requires": { - "unique-slug": "2.0.0" - } + "util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=", + "dev": true }, - "unique-slug": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", - "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", "requires": { - "imurmurhash": "0.1.4" + "define-properties": "1.1.2", + "object.getownpropertydescriptors": "2.0.3" } }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "uws": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz", + "integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=", + "optional": true }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" } }, - "url-loader": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.6.2.tgz", - "integrity": "sha512-h3qf9TNn53BpuXTTcpC+UehiRrl0Cv45Yr/xWayApjw6G8Bg2dGke7rIwDQ39piciWCWrC+WiqLjOh3SUp9n0Q==", - "requires": { - "loader-utils": "1.1.0", - "mime": "1.6.0", - "schema-utils": "0.3.0" - } + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "url-parse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz", - "integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==", + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "querystringify": "1.0.0", - "requires-port": "1.0.0" + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" }, "dependencies": { - "querystringify": { + "assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", - "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=" + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" } } }, - "use": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", - "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", "requires": { - "define-property": "0.2.5", - "isobject": "3.0.1", - "lazy-cache": "2.0.2" + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "watchpack": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.5.0.tgz", + "integrity": "sha512-RSlipNQB1u48cq0wH/BNfCu1tD/cJ8ydFIkNYhp9o+3d+8unClkIovpW5qpFPgmL9OE48wfAnlZydXByWP82AA==", + "requires": { + "chokidar": "2.0.3", + "graceful-fs": "4.1.11", + "neo-async": "2.5.0" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "requires": { - "is-descriptor": "0.1.6" + "micromatch": "3.1.10", + "normalize-path": "2.1.1" } }, - "is-descriptor": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.1", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.1", + "fsevents": "1.1.3", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.0.4" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, "isobject": { @@ -12153,171 +12042,36 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, - "lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "requires": { - "set-getter": "0.1.0" - } - } - } - }, - "useragent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", - "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", - "requires": { - "lru-cache": "2.2.4", - "tmp": "0.0.33" - }, - "dependencies": { - "lru-cache": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", - "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=" - } - } - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util-extend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", - "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=", - "dev": true - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "uws": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz", - "integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=", - "optional": true - }, - "v8-profiler": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.7.0.tgz", - "integrity": "sha1-6DgcvrtbX9DKjSsJ9qAYGhWNs00=", - "optional": true, - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - } - }, - "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "vendors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", - "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "vlq": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", - "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "requires": { - "indexof": "0.0.1" - } - }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" - }, - "watchpack": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", - "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", - "requires": { - "async": "2.6.0", - "chokidar": "1.7.0", - "graceful-fs": "4.1.11" - }, - "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "requires": { - "lodash": "4.17.4" + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" } } } }, "wbuf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz", - "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "requires": { "minimalistic-assert": "1.0.0" } @@ -12373,224 +12127,312 @@ "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", "requires": { "sax": "0.6.1", - "xmlbuilder": "9.0.4" + "xmlbuilder": "9.0.7" } } } }, "webpack": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz", - "integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==", - "requires": { - "acorn": "5.4.1", - "acorn-dynamic-import": "2.0.2", - "ajv": "5.5.2", - "ajv-keywords": "2.1.1", - "async": "2.6.0", - "enhanced-resolve": "3.4.1", - "escope": "3.6.0", - "interpret": "1.1.0", - "json-loader": "0.5.7", - "json5": "0.5.1", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.5.0.tgz", + "integrity": "sha512-6GrZsvQJnG7o7mjbfjp6s5CyMfdopjt1A/X8LcYwceis9ySjqBX6Lusso2wNZ06utHj2ZvfL6L3f7hfgVeJP6g==", + "requires": { + "acorn": "5.5.3", + "acorn-dynamic-import": "3.0.0", + "ajv": "6.4.0", + "ajv-keywords": "3.1.0", + "chrome-trace-event": "0.1.2", + "enhanced-resolve": "4.0.0", + "eslint-scope": "3.7.1", "loader-runner": "2.3.0", "loader-utils": "1.1.0", "memory-fs": "0.4.1", + "micromatch": "3.1.10", "mkdirp": "0.5.1", + "neo-async": "2.5.0", "node-libs-browser": "2.1.0", - "source-map": "0.5.7", - "supports-color": "4.5.0", - "tapable": "0.2.8", - "uglifyjs-webpack-plugin": "0.4.6", - "watchpack": "1.4.0", - "webpack-sources": "1.1.0", - "yargs": "8.0.2" + "schema-utils": "0.4.5", + "tapable": "1.0.0", + "uglifyjs-webpack-plugin": "1.2.4", + "watchpack": "1.5.0", + "webpack-sources": "1.1.0" }, "dependencies": { "acorn": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", - "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==" + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "ajv": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.4.0.tgz", + "integrity": "sha1-06/3jpJ3VJdx2vAWTP9ISCt1T8Y=", "requires": { - "lodash": "4.17.4" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1", + "uri-js": "3.0.2" } }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "braces": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.1", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" }, "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "2.0.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" - } - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } } }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "requires": { - "pify": "2.3.0" + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } } }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.4.0", - "path-type": "2.0.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" + "kind-of": "3.2.2" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "ansi-regex": "3.0.0" + "is-buffer": "1.1.6" } } } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "requires": { - "has-flag": "2.0.0" - } + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, - "uglifyjs-webpack-plugin": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", - "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "requires": { - "source-map": "0.5.7", - "uglify-js": "2.8.29", - "webpack-sources": "1.1.0" + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.1", + "to-regex": "3.0.2" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", "requires": { - "camelcase": "4.1.0", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "read-pkg-up": "2.0.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "7.0.0" + "ajv": "6.4.0", + "ajv-keywords": "3.1.0" } }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.4.tgz", + "integrity": "sha512-z0IbjpW8b3O/OVn+TTZN4pI29RN1zktFBXLIzzfZ+++cUtZ1ERSlLWgpE/5OERuEUs1ijVQnpYAkSlpoVmQmSQ==", "requires": { - "camelcase": "4.1.0" + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.4.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.5.4" } } } @@ -12620,27 +12462,36 @@ } }, "webpack-dev-middleware": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", - "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.1.0.tgz", + "integrity": "sha512-UtAd5+J3IihQilwxOESie2BKaeo37yjmMSfV5G+UGEwPwqgL9+L/rShvjrfse8ARSRQGd3QwN2ANSk++KYZizQ==", "requires": { + "loud-rejection": "1.6.0", "memory-fs": "0.4.1", - "mime": "1.6.0", + "mime": "2.2.0", "path-is-absolute": "1.0.1", "range-parser": "1.2.0", - "time-stamp": "2.0.0" + "url-join": "4.0.0", + "webpack-log": "1.1.2" + }, + "dependencies": { + "mime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", + "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==" + } } }, "webpack-dev-server": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.11.0.tgz", - "integrity": "sha512-lXzc36DGjKUVinETNmDWhfZFRbHMhatuF+lKex+czqY+JVe0Qf2V+Ig6/svDdbt/DmXFXuLQmSqhncYCqYf3qA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.1.tgz", + "integrity": "sha512-u5lz6REb3+KklgSIytUIOrmWgnpgFmfj/+I+GBXurhEoCsHXpG9twk4NO3bsu72GC9YtxIsiavjfRdhmNt0A/A==", "requires": { "ansi-html": "0.0.7", "array-includes": "3.0.3", "bonjour": "3.5.0", - "chokidar": "2.0.0", - "compression": "1.7.1", + "chokidar": "2.0.3", + "compression": "1.7.2", "connect-history-api-fallback": "1.5.0", "debug": "3.1.0", "del": "3.0.0", @@ -12651,18 +12502,19 @@ "internal-ip": "1.2.0", "ip": "1.1.5", "killable": "1.0.0", - "loglevel": "1.6.0", + "loglevel": "1.6.1", "opn": "5.1.0", "portfinder": "1.0.13", - "selfsigned": "1.10.1", + "selfsigned": "1.10.2", "serve-index": "1.9.1", "sockjs": "0.3.19", "sockjs-client": "1.1.4", "spdy": "3.4.7", - "strip-ansi": "4.0.0", - "supports-color": "5.1.0", - "webpack-dev-middleware": "1.12.2", - "yargs": "6.6.0" + "strip-ansi": "3.0.1", + "supports-color": "5.3.0", + "webpack-dev-middleware": "3.0.1", + "webpack-log": "1.1.2", + "yargs": "9.0.1" }, "dependencies": { "ansi-regex": { @@ -12675,7 +12527,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "requires": { - "micromatch": "3.1.4", + "micromatch": "3.1.10", "normalize-path": "2.1.1" } }, @@ -12690,9 +12542,9 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "braces": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.0.tgz", - "integrity": "sha512-P4O8UQRdGiMLWSizsApmXVQDBS6KCt7dSexgLKBmH5Hr1CZq7vsnscFh8oR1sP1ab1Zj0uCHCEzZeV6SfUf3rA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", "requires": { "arr-flatten": "1.1.0", "array-unique": "0.3.2", @@ -12700,26 +12552,45 @@ "extend-shallow": "2.0.1", "fill-range": "4.0.0", "isobject": "3.0.1", + "kind-of": "6.0.2", "repeat-element": "1.1.2", "snapdragon": "0.8.1", "snapdragon-node": "2.1.1", "split-string": "3.1.0", - "to-regex": "3.0.1" + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } } }, "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" }, "chokidar": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.0.tgz", - "integrity": "sha512-OgXCNv2U6TnG04D3tth0gsvdbV4zdbxFG3sYUqcoQMoEFVd1j1pZR6TZ8iknC45o9IJ6PeQI/J6wT/+cHcniAw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", "requires": { "anymatch": "2.0.0", "async-each": "1.0.1", - "braces": "2.3.0", + "braces": "2.3.1", "fsevents": "1.1.3", "glob-parent": "3.1.0", "inherits": "2.0.3", @@ -12727,7 +12598,8 @@ "is-glob": "4.0.0", "normalize-path": "2.1.1", "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" + "readdirp": "2.1.0", + "upath": "1.0.4" } }, "cliui": { @@ -12740,17 +12612,14 @@ "wrap-ansi": "2.1.0" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "ansi-regex": "2.1.1" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } } } @@ -12785,9 +12654,9 @@ "define-property": "0.2.5", "extend-shallow": "2.0.1", "posix-character-classes": "0.1.1", - "regex-not": "1.0.0", + "regex-not": "1.0.2", "snapdragon": "0.8.1", - "to-regex": "3.0.1" + "to-regex": "3.0.2" }, "dependencies": { "debug": { @@ -12805,22 +12674,63 @@ "requires": { "is-descriptor": "0.1.6" } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, "extglob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.2.tgz", - "integrity": "sha512-I0+eZBH+jFGL8F5BnIz2ON2nKCjTS3AS3H/5PeSmCp7UVC70Ym8IhdRiQly2juKYQ//f7z1aj1BRpQniFJoU1w==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "requires": { "array-unique": "0.3.2", "define-property": "1.0.0", "expand-brackets": "2.1.4", "extend-shallow": "2.0.1", "fragment-cache": "0.2.1", - "regex-not": "1.0.0", + "regex-not": "1.0.2", "snapdragon": "0.8.1", - "to-regex": "3.0.1" + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } } }, "fill-range": { @@ -12832,6 +12742,24 @@ "is-number": "3.0.0", "repeat-string": "1.6.1", "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" } }, "glob-parent": { @@ -12873,29 +12801,48 @@ } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" }, - "is-descriptor": { + "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "kind-of": "3.2.2" }, "dependencies": { "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } } } }, @@ -12940,24 +12887,72 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, "micromatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.4.tgz", - "integrity": "sha512-kFRtviKYoAJT+t7HggMl0tBFGNAKLw/S7N+CO9qfEQyisob1Oy4pao+geRbkyeEd+V9aOkvZ4mhuyPvI/q9Sfg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "requires": { "arr-diff": "4.0.0", "array-unique": "0.3.2", - "braces": "2.3.0", - "define-property": "1.0.0", - "extend-shallow": "2.0.1", - "extglob": "2.0.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", "fragment-cache": "0.2.1", "kind-of": "6.0.2", - "nanomatch": "1.2.6", + "nanomatch": "1.2.9", "object.pick": "1.3.0", - "regex-not": "1.0.0", + "regex-not": "1.0.2", "snapdragon": "0.8.1", - "to-regex": "3.0.1" + "to-regex": "3.0.2" + } + }, + "mime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", + "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==" + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } } }, "pify": { @@ -12965,58 +12960,140 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", "requires": { - "ansi-regex": "3.0.0" + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", "requires": { - "has-flag": "2.0.0" + "has-flag": "3.0.0" + } + }, + "url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=" + }, + "webpack-dev-middleware": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.0.1.tgz", + "integrity": "sha512-JCturcEZNGA0KHEpOJVRTC/VVazTcPfpR9c1Au6NO9a+jxCRchMi87Qe7y3JeOzc0v5eMMKpuGBnPdN52NA+CQ==", + "requires": { + "loud-rejection": "1.6.0", + "memory-fs": "0.4.1", + "mime": "2.2.0", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "url-join": "4.0.0", + "webpack-log": "1.1.2" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-9.0.1.tgz", + "integrity": "sha1-UqzCP+7Kw0BCB47njAwAf1CF20w=", "requires": { - "camelcase": "3.0.0", + "camelcase": "4.1.0", "cliui": "3.2.0", "decamelize": "1.2.0", "get-caller-file": "1.0.2", - "os-locale": "1.4.0", - "read-pkg-up": "1.0.1", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", "require-directory": "2.1.1", "require-main-filename": "1.0.1", "set-blocking": "2.0.0", - "string-width": "1.0.2", - "which-module": "1.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", "y18n": "3.2.1", - "yargs-parser": "4.2.1" + "yargs-parser": "7.0.0" } }, "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", "requires": { - "camelcase": "3.0.0" + "camelcase": "4.1.0" } } } }, + "webpack-log": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.1.2.tgz", + "integrity": "sha512-B53SD4N4BHpZdUwZcj4st2QT7gVfqZtqHDruC1N+K2sciq0Rt/3F1Dx6RlylVkcrToMLTaiaeT48k9Lq4iDVDA==", + "requires": { + "chalk": "2.2.2", + "log-symbols": "2.2.0", + "loglevelnext": "1.0.3", + "uuid": "3.1.0" + } + }, "webpack-merge": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.1.tgz", - "integrity": "sha512-geQsZ86YkXOVOjvPC5yv3JSNnL6/X3Kzh935AQ/gJNEYXEfJDQFu/sdFuktS9OW2JcH/SJec8TGfRdrpHshH7A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.2.tgz", + "integrity": "sha512-/0QYwW/H1N/CdXYA2PNPVbsxO3u2Fpz34vs72xm03SRfg6bMNGfMJIQEpQjKRvkG2JvT6oRJFpDtSrwbX8Jzvw==", "requires": { - "lodash": "4.17.4" + "lodash": "4.17.5" + }, + "dependencies": { + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + } } }, "webpack-sources": { @@ -13036,9 +13113,9 @@ } }, "webpack-subresource-integrity": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.0.3.tgz", - "integrity": "sha1-wGBtQAkLBwzeQovsjfNgMhbkcus=", + "version": "1.1.0-rc.4", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.4.tgz", + "integrity": "sha1-xcTj1pD50vZKlVDgeodn+Xlqpdg=", "requires": { "webpack-core": "0.6.9" } @@ -13048,7 +13125,7 @@ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", "requires": { - "http-parser-js": "0.4.9", + "http-parser-js": "0.4.11", "websocket-extensions": "0.1.3" } }, @@ -13063,11 +13140,6 @@ "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=", "optional": true }, - "whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=" - }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", @@ -13089,10 +13161,48 @@ "string-width": "1.0.2" } }, + "widest-line": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "requires": { + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "optional": true }, "wordwrap": { "version": "0.0.3", @@ -13100,12 +13210,27 @@ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" }, "worker-farm": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.2.tgz", - "integrity": "sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.5.4.tgz", + "integrity": "sha512-ITyClEvcfv0ozqJl1vmWFWhvI+OIrkbInYqkEPE50wFPXj8J9Gd3FYf8+CkZJXJJsQBYe+2DvmoK9Zhx5w8W+w==", "requires": { - "errno": "0.1.4", + "errno": "0.1.7", "xtend": "4.0.1" + }, + "dependencies": { + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "requires": { + "prr": "1.0.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + } } }, "wrap-ansi": { @@ -13122,6 +13247,16 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, "ws": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", @@ -13132,6 +13267,16 @@ "ultron": "1.1.1" } }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, "xml-char-classes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", @@ -13143,13 +13288,13 @@ "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", "requires": { "sax": "1.2.4", - "xmlbuilder": "9.0.4" + "xmlbuilder": "9.0.7" } }, "xmlbuilder": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", - "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=" + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmlhttprequest-ssl": { "version": "1.5.4", @@ -13189,6 +13334,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "optional": true, "requires": { "camelcase": "1.2.1", "cliui": "2.1.0", @@ -13199,7 +13345,8 @@ "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "optional": true } } }, diff --git a/package.json b/package.json index 5764f15eff..f1317a0922 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,17 @@ "buildifier": "find . -type f \\( -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/buildifier", "templates": "node ./bin/devkit-admin templates", "test": "node ./bin/devkit-admin test", + "test-large": "node ./bin/devkit-admin test --large --spec-reporter", "test:watch": "nodemon --watch packages -e ts ./bin/devkit-admin test", "validate": "node ./bin/devkit-admin validate", "validate-commits": "./bin/devkit-admin validate-commits", "integration": "npm run build && npm run integration:build-optimizer", "integration:build-optimizer": "npm run integration:build-optimizer:simple && npm run integration:build-optimizer:aio", - "integration:build-optimizer:simple": "cd tests/@angular_devkit/build_optimizer/webpack/simple-app && npm i -q && npm run reinstall-bo && npm run e2e && npm run benchmark", - "integration:build-optimizer:aio": "cd tests/@angular_devkit/build_optimizer/webpack/aio-app && npm i -q && npm run reinstall-bo && npm run e2e && npm run benchmark", - "postinstall": "npm run admin -- patch-dependencies", - "prepush": "node ./bin/devkit-admin hooks/pre-push" + "integration:build-optimizer:simple": "cd tests/@angular_devkit/build_optimizer/webpack/simple-app && npm i -q --no-save && npm run e2e && npm run benchmark", + "integration:build-optimizer:aio": "cd tests/@angular_devkit/build_optimizer/webpack/aio-app && npm i -q --no-save && npm run e2e && npm run benchmark", + "prepush": "node ./bin/devkit-admin hooks/pre-push", + "webdriver-update-appveyor": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.37", + "webdriver-update-circleci": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.33" }, "repository": { "type": "git", @@ -50,18 +52,22 @@ }, "homepage": "https://github.com/angular/devkit", "dependencies": { - "@angular/common": "^5.2.1", - "@angular/compiler": "^5.2.1", - "@angular/compiler-cli": "^5.2.1", - "@angular/core": "^5.2.1", - "@angular/platform-browser": "^5.2.1", - "@angular/platform-browser-dynamic": "^5.2.1", - "@angular/service-worker": "^5.2.1", + "@angular/animations": "6.0.0-rc.3", + "@angular/cdk": "6.0.0-rc.1", + "@angular/common": "6.0.0-rc.3", + "@angular/compiler": "6.0.0-rc.3", + "@angular/compiler-cli": "6.0.0-rc.3", + "@angular/core": "6.0.0-rc.3", + "@angular/http": "6.0.0-rc.3", + "@angular/material": "6.0.0-rc.1", + "@angular/platform-browser": "6.0.0-rc.3", + "@angular/platform-browser-dynamic": "6.0.0-rc.3", + "@angular/platform-server": "6.0.0-rc.3", + "@angular/router": "6.0.0-rc.3", + "@angular/service-worker": "6.0.0-rc.3", "@ngtools/json-schema": "^1.0.9", - "@ngtools/webpack": "^1.10.0-rc.0", - "@types/common-tags": "^1.4.0", "@types/copy-webpack-plugin": "^4.0.1", - "@types/denodeify": "^1.2.31", + "@types/express": "^4.11.1", "@types/glob": "^5.0.29", "@types/istanbul": "^0.4.29", "@types/jasmine": "^2.5.47", @@ -71,29 +77,30 @@ "@types/request": "^2.47.0", "@types/semver": "^5.3.30", "@types/source-map": "0.5.2", - "@types/webpack": "^3.0.2", + "@types/webpack": "^3.8.2", "@types/webpack-sources": "^0.1.3", "ajv": "^5.5.1", - "autoprefixer": "^7.2.3", + "autoprefixer": "^8.1.0", + "bootstrap": "^4.0.0", + "cache-loader": "^1.2.2", "chalk": "~2.2.2", "chokidar": "^1.7.0", - "circular-dependency-plugin": "^4.3.0", - "clean-css": "^4.1.9", - "codelyzer": "^4.0.2", - "common-tags": "^1.5.1", + "circular-dependency-plugin": "^5.0.0", + "clean-css": "^4.1.11", + "codelyzer": "^4.2.1", "conventional-changelog": "^1.1.0", - "copy-webpack-plugin": "^4.2.3", - "css-loader": "^0.28.7", - "denodeify": "^1.2.1", - "exports-loader": "^0.6.4", - "extract-text-webpack-plugin": "^3.0.2", - "file-loader": "^1.1.5", + "copy-webpack-plugin": "^4.5.0", + "express": "^4.16.2", + "file-loader": "^1.1.11", + "font-awesome": "^4.7.0", "glob": "^7.0.3", - "html-webpack-plugin": "^2.30.1", + "html-webpack-plugin": "^3.0.6", "husky": "^0.14.3", "istanbul": "^0.4.5", + "istanbul-instrumenter-loader": "^3.0.1", "jasmine": "^2.6.0", "jasmine-spec-reporter": "^3.2.0", + "jquery": "^3.3.1", "karma": "~2.0.0", "karma-chrome-launcher": "~2.2.0", "karma-cli": "~1.0.1", @@ -101,54 +108,60 @@ "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "karma-source-map-support": "^1.2.0", - "less": "^2.7.3", - "less-loader": "^4.0.5", - "license-webpack-plugin": "^1.1.1", + "less": "^3.0.1", + "less-loader": "^4.1.0", + "license-webpack-plugin": "^1.2.3", "loader-utils": "^1.1.0", "lodash": "^4.17.4", + "material-design-icons": "^3.0.1", "memory-fs": "^0.4.1", + "mini-css-extract-plugin": "~0.3.0", "minimatch": "^3.0.4", "minimist": "^1.2.0", + "ng-packagr": "^2.4.1", "node-sass": "^4.7.2", "opn": "^5.1.0", + "parse5": "^4.0.0", + "popper.js": "^1.14.1", "portfinder": "^1.0.13", - "postcss-import": "^11.0.0", - "postcss-loader": "^2.0.10", - "postcss-url": "^7.3.0", - "protractor": "^5.1.2", + "postcss": "^6.0.19", + "postcss-import": "^11.1.0", + "postcss-loader": "^2.1.1", + "postcss-url": "^7.3.1", + "protractor": "^5.3.1", "raw-loader": "^0.5.1", "request": "^2.83.0", - "rxjs": "^5.5.6", - "sass-loader": "^6.0.6", + "resolve": "^1.5.0", + "rxjs": "^6.0.0-tactical-rc.1", + "sass-loader": "^6.0.7", "semver": "^5.3.0", "semver-intersect": "^1.1.2", "silent-error": "^1.1.0", "source-map": "^0.5.6", - "source-map-loader": "^0.2.3", "source-map-support": "^0.5.0", - "style-loader": "^0.19.1", + "stats-webpack-plugin": "^0.6.2", + "style-loader": "^0.20.2", "stylus": "^0.54.5", - "stylus-loader": "^3.0.1", + "stylus-loader": "^3.0.2", + "symbol-observable": "^1.2.0", "tar": "^3.1.5", "temp": "^0.8.3", "tree-kill": "^1.2.0", "ts-node": "^5.0.0", "tslint": "^5.9.1", + "tsutils": "~2.22.2", "typescript": "~2.7.2", - "uglifyjs-webpack-plugin": "^1.1.6", - "url-loader": "^0.6.2", - "webpack": "^3.10.0", - "webpack-dev-middleware": "^1.12.2", - "webpack-dev-server": "^2.11.0", - "webpack-merge": "^4.1.1", - "webpack-sources": "^1.0.1", - "webpack-subresource-integrity": "^1.0.3", + "uglifyjs-webpack-plugin": "^1.2.2", + "url-loader": "^1.0.1", + "webpack": "~4.5.0", + "webpack-dev-middleware": "^3.1.0", + "webpack-dev-server": "^3.1.1", + "webpack-merge": "^4.1.2", + "webpack-sources": "^1.1.0", + "webpack-subresource-integrity": "^1.1.0-rc.4", "zone.js": "^0.8.19" }, "devDependencies": { "license-checker": "^16.0.0" - }, - "optionalDependencies": { - "v8-profiler": "^5.7.0" } } diff --git a/packages/_/devkit/collection.json b/packages/_/devkit/collection.json new file mode 100644 index 0000000000..0353b64430 --- /dev/null +++ b/packages/_/devkit/collection.json @@ -0,0 +1,9 @@ +{ + "schematics": { + "package": { + "factory": "./package/factory", + "schema": "./package/schema.json", + "description": "Create an empty schematic project or add a blank schematic to the current project." + } + } +} diff --git a/packages/_/devkit/package.json b/packages/_/devkit/package.json new file mode 100644 index 0000000000..23b4a671ac --- /dev/null +++ b/packages/_/devkit/package.json @@ -0,0 +1,14 @@ +{ + "name": "devkit", + "version": "0.0.0", + "description": "Schematics specific to DevKit (used internally, not released)", + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "schematics": "./collection.json", + "private": true, + "dependencies": { + "@angular-devkit/core": "0.0.0", + "@angular-devkit/schematics": "0.0.0" + } +} diff --git a/packages/_/devkit/package/factory.ts b/packages/_/devkit/package/factory.ts new file mode 100644 index 0000000000..fac301e95f --- /dev/null +++ b/packages/_/devkit/package/factory.ts @@ -0,0 +1,105 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonAstObject, JsonValue, parseJsonAst } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + Tree, + UpdateRecorder, + apply, + chain, + mergeWith, + template, + url, +} from '@angular-devkit/schematics'; +import { Schema } from './schema'; + + +function appendPropertyInAstObject( + recorder: UpdateRecorder, + node: JsonAstObject, + propertyName: string, + value: JsonValue, + indent = 4, +) { + const indentStr = '\n' + new Array(indent + 1).join(' '); + + if (node.properties.length > 0) { + // Insert comma. + const last = node.properties[node.properties.length - 1]; + recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ','); + } + + recorder.insertLeft( + node.end.offset - 1, + ' ' + + `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}` + + indentStr.slice(0, -2), + ); +} + +function addPackageToMonorepo(options: Schema, path: string): Rule { + return (tree: Tree) => { + const collectionJsonContent = tree.read('/.monorepo.json'); + if (!collectionJsonContent) { + throw new Error('Could not find monorepo.json'); + } + const collectionJsonAst = parseJsonAst(collectionJsonContent.toString('utf-8')); + if (collectionJsonAst.kind !== 'object') { + throw new Error('Invalid monorepo content.'); + } + + const packages = collectionJsonAst.properties.find(x => x.key.value == 'packages'); + if (!packages) { + throw new Error('Cannot find packages key in monorepo.'); + } + if (packages.value.kind != 'object') { + throw new Error('Invalid packages key.'); + } + + const readmeUrl = `https://github.com/angular/devkit/blob/master/${path}/README.md`; + + const recorder = tree.beginUpdate('/.monorepo.json'); + appendPropertyInAstObject( + recorder, + packages.value, + options.name, + { + name: options.displayName, + links: [{ label: 'README', url: readmeUrl }], + version: '0.0.1', + hash: '', + }, + ); + tree.commitUpdate(recorder); + }; +} + + +export default function (options: Schema): Rule { + const path = 'packages/' + + options.name + .replace(/^@/, '') + .replace(/-/g, '_'); + + // Verify if we need to create a full project, or just add a new schematic. + return (tree: Tree, context: SchematicContext) => { + const source = apply(url('./project-files'), [ + template({ + ...options as object, + dot: '.', + path, + }), + ]); + + return chain([ + mergeWith(source), + addPackageToMonorepo(options, path), + ])(tree, context); + }; +} diff --git a/packages/_/devkit/package/project-files/__path__/README.md b/packages/_/devkit/package/project-files/__path__/README.md new file mode 100644 index 0000000000..f4b4254753 --- /dev/null +++ b/packages/_/devkit/package/project-files/__path__/README.md @@ -0,0 +1,3 @@ +# <%= displayName %> + +Work in progress diff --git a/packages/_/devkit/package/project-files/__path__/package.json b/packages/_/devkit/package/project-files/__path__/package.json new file mode 100644 index 0000000000..7126e5f406 --- /dev/null +++ b/packages/_/devkit/package/project-files/__path__/package.json @@ -0,0 +1,16 @@ +{ + "name": "<%= name %>", + "version": "0.0.0", + "description": "<%= description %>", + "main": "src/index.js", + "typings": "src/index.d.ts", + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "keywords": [ + ], + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "0.0.0" + } +} diff --git a/packages/angular_devkit/build_webpack/src/index.ts b/packages/_/devkit/package/project-files/__path__/src/index.ts similarity index 54% rename from packages/angular_devkit/build_webpack/src/index.ts rename to packages/_/devkit/package/project-files/__path__/src/index.ts index 8101bf131e..258badcf91 100644 --- a/packages/angular_devkit/build_webpack/src/index.ts +++ b/packages/_/devkit/package/project-files/__path__/src/index.ts @@ -6,9 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './browser'; -export * from './dev-server'; -export * from './extract-i18n'; -export * from './karma'; -export * from './protractor'; -export * from './tslint'; +// TODO: Make this useful (and awesome). +export default 1; diff --git a/packages/_/devkit/package/schema.d.ts b/packages/_/devkit/package/schema.d.ts new file mode 100644 index 0000000000..0f2b32ac37 --- /dev/null +++ b/packages/_/devkit/package/schema.d.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + name: string; + description: string; + displayName: string; +} diff --git a/packages/_/devkit/package/schema.json b/packages/_/devkit/package/schema.json new file mode 100644 index 0000000000..1689719bd9 --- /dev/null +++ b/packages/_/devkit/package/schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsSchematicSchema", + "title": "DevKit Package Schematic Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The package name for the new schematic.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "description": { + "type": "string", + "description": "The description of the new package" + }, + "displayName": { + "type": "string", + "$default": { + "$source": "interpolation", + "value": "${name}" + }, + "description": "The human readable name." + } + } +} diff --git a/packages/angular/pwa/collection.json b/packages/angular/pwa/collection.json new file mode 100644 index 0000000000..f0fc0030d5 --- /dev/null +++ b/packages/angular/pwa/collection.json @@ -0,0 +1,11 @@ +{ + "schematics": { + "ng-add": { + "factory": "./pwa", + "description": "Update an application with PWA defaults.", + "schema": "./pwa/schema.json", + "private": true, + "hidden": true + } + } +} diff --git a/packages/angular/pwa/package.json b/packages/angular/pwa/package.json new file mode 100644 index 0000000000..946e0afd70 --- /dev/null +++ b/packages/angular/pwa/package.json @@ -0,0 +1,20 @@ +{ + "name": "@angular/pwa", + "version": "0.0.0", + "description": "PWA schematics for Angular", + "keywords": [ + "blueprints", + "code generation", + "schematics" + ], + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "schematics": "./collection.json", + "dependencies": { + "@angular-devkit/core": "0.0.0", + "@angular-devkit/schematics": "0.0.0", + "@schematics/angular": "0.0.0", + "typescript": "~2.6.2" + } +} \ No newline at end of file diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png b/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png new file mode 100644 index 0000000000..9f9241f0be Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-128x128.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png b/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png new file mode 100644 index 0000000000..4a5f8c1638 Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-144x144.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png b/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png new file mode 100644 index 0000000000..34a1a8d645 Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-152x152.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png b/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png new file mode 100644 index 0000000000..9172e5dd29 Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-192x192.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png b/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png new file mode 100644 index 0000000000..e54e8d3eaf Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-384x384.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png b/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png new file mode 100644 index 0000000000..51ee297df1 Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-512x512.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png b/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png new file mode 100644 index 0000000000..2814a3f30c Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-72x72.png differ diff --git a/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png b/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png new file mode 100644 index 0000000000..d271025c4f Binary files /dev/null and b/packages/angular/pwa/pwa/files/assets/icons/icon-96x96.png differ diff --git a/packages/angular/pwa/pwa/files/assets/manifest.json b/packages/angular/pwa/pwa/files/assets/manifest.json new file mode 100644 index 0000000000..f9c9b4666f --- /dev/null +++ b/packages/angular/pwa/pwa/files/assets/manifest.json @@ -0,0 +1,52 @@ +{ + "name": "<%= title %>", + "short_name": "<%= title %>", + "theme_color": "#1976d2", + "background_color": "#fafafa", + "display": "browser", + "Scope": "/", + "start_url": "/", + "icons": [ + { + "src": "images/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "images/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "images/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "images/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "images/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "images/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "images/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "images/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "splash_pages": null +} \ No newline at end of file diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts new file mode 100644 index 0000000000..11b30db275 --- /dev/null +++ b/packages/angular/pwa/pwa/index.ts @@ -0,0 +1,55 @@ +/** +* @license +* Copyright Google Inc. All Rights Reserved. +* +* Use of this source code is governed by an MIT-style license that can be +* found in the LICENSE file at https://angular.io/license +*/ +import { Path, join } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + Tree, + apply, + branchAndMerge, + chain, + externalSchematic, + mergeWith, + move, + template, + url, +} from '@angular-devkit/schematics'; +import { getWorkspace } from '../utility/config'; +import { Schema as PwaOptions } from './schema'; + + +function addServiceWorker(options: PwaOptions): Rule { + return (host: Tree, context: SchematicContext) => { + context.logger.debug('Adding service worker...'); + + return externalSchematic('@schematics/angular', 'service-worker', options)(host, context); + }; +} + +export default function (options: PwaOptions): Rule { + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + const project = workspace.projects[options.project]; + + const assetPath = join(project.root as Path, 'src', 'assets'); + + const tempalteSource = apply(url('./files/assets'), [ + template({ + ...options, + }), + move(assetPath), + ]); + + return chain([ + addServiceWorker(options), + branchAndMerge(chain([ + mergeWith(tempalteSource), + ])), + ])(host, context); + }; +} diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts new file mode 100644 index 0000000000..13a995f9f7 --- /dev/null +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -0,0 +1,80 @@ +/** + * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Schema as PwaOptions } from './schema'; + + +// tslint:disable:max-line-length +describe('PWA Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@angular/pwa', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: PwaOptions = { + project: 'bar', + target: 'build', + configuration: 'production', + title: 'Fake Title', + }; + + let appTree: UnitTestTree; + + // tslint:disable-next-line:no-any + const workspaceOptions: any = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + // tslint:disable-next-line:no-any + const appOptions: any = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + }; + + beforeEach(() => { + appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'workspace', workspaceOptions); + appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'application', appOptions, appTree); + }); + + it('should run the service worker schematic', () => { + const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + const configText = tree.readContent('/angular.json'); + const config = JSON.parse(configText); + const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker; + expect(swFlag).toEqual(true); + }); + + it('should create icon files', () => { + const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; + const iconPath = '/projects/bar/src/assets/icons/icon-'; + const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + dimensions.forEach(d => { + const path = `${iconPath}${d}x${d}.png`; + expect(tree.exists(path)).toEqual(true); + }); + }); + + it('should create a manifest file', () => { + const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + expect(tree.exists('/projects/bar/src/assets/manifest.json')).toEqual(true); + }); + + it('should set the name & short_name in the manifest file', () => { + const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + const manifestText = tree.readContent('/projects/bar/src/assets/manifest.json'); + const manifest = JSON.parse(manifestText); + expect(manifest.name).toEqual(defaultOptions.title); + expect(manifest.short_name).toEqual(defaultOptions.title); + }); +}); diff --git a/packages/angular/pwa/pwa/schema.d.ts b/packages/angular/pwa/pwa/schema.d.ts new file mode 100644 index 0000000000..1331fdaebf --- /dev/null +++ b/packages/angular/pwa/pwa/schema.d.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The name of the project. + */ + project: string; + /** + * ": "The target to apply service worker to. + */ + target: string; + /** + * The configuration to apply service worker to. + */ + configuration: string; + /** + * The title of the application. + */ + title?: string; +} diff --git a/packages/angular/pwa/pwa/schema.json b/packages/angular/pwa/pwa/schema.json new file mode 100644 index 0000000000..05a34fcbd6 --- /dev/null +++ b/packages/angular/pwa/pwa/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularPWA", + "title": "Angular PWA Options Schema", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project." + }, + "target": { + "type": "string", + "description": "The target to apply service worker to.", + "default": "build" + }, + "configuration": { + "type": "string", + "description": "The configuration to apply service worker to.", + "default": "production" + }, + "title": { + "type": "string", + "description": "The title of the application." + } + }, + "required": [ + "project" + ] +} \ No newline at end of file diff --git a/packages/angular/pwa/utility/config.ts b/packages/angular/pwa/utility/config.ts new file mode 100644 index 0000000000..3add859083 --- /dev/null +++ b/packages/angular/pwa/utility/config.ts @@ -0,0 +1,487 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental } from '@angular-devkit/core'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; + + +// The interfaces below are generated from the Angular CLI configuration schema +// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json +export interface AppConfig { + /** + * Name of the app. + */ + name?: string; + /** + * Directory where app files are placed. + */ + appRoot?: string; + /** + * The root directory of the app. + */ + root?: string; + /** + * The output directory for build results. + */ + outDir?: string; + /** + * List of application assets. + */ + assets?: (string | { + /** + * The pattern to match. + */ + glob?: string; + /** + * The dir to search within. + */ + input?: string; + /** + * The output path (relative to the outDir). + */ + output?: string; + })[]; + /** + * URL where files will be deployed. + */ + deployUrl?: string; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The runtime platform of the app. + */ + platform?: ('browser' | 'server'); + /** + * The name of the start HTML file. + */ + index?: string; + /** + * The name of the main entry-point file. + */ + main?: string; + /** + * The name of the polyfills file. + */ + polyfills?: string; + /** + * The name of the test entry-point file. + */ + test?: string; + /** + * The name of the TypeScript configuration file. + */ + tsconfig?: string; + /** + * The name of the TypeScript configuration file for unit tests. + */ + testTsconfig?: string; + /** + * The prefix to apply to generated selectors. + */ + prefix?: string; + /** + * Experimental support for a service worker from @angular/service-worker. + */ + serviceWorker?: boolean; + /** + * Global styles to be included in the build. + */ + styles?: (string | { + input?: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: { + /** + * Paths to include. Paths will be resolved to project root. + */ + includePaths?: string[]; + }; + /** + * Global scripts to be included in the build. + */ + scripts?: (string | { + input: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Source file for environment config. + */ + environmentSource?: string; + /** + * Name and corresponding file for environment config. + */ + environments?: { + [name: string]: any; // tslint:disable-line:no-any + }; + appShell?: { + app: string; + route: string; + }; +} + +export interface CliConfig { + $schema?: string; + /** + * The global configuration of the project. + */ + project?: { + /** + * The name of the project. + */ + name?: string; + /** + * Whether or not this project was ejected. + */ + ejected?: boolean; + }; + /** + * Properties of the different applications in this project. + */ + apps?: AppConfig[]; + /** + * Configuration for end-to-end tests. + */ + e2e?: { + protractor?: { + /** + * Path to the config file. + */ + config?: string; + }; + }; + /** + * Properties to be passed to TSLint. + */ + lint?: { + /** + * File glob(s) to lint. + */ + files?: (string | string[]); + /** + * Location of the tsconfig.json project file. + * Will also use as files to lint if 'files' property not present. + */ + project: string; + /** + * Location of the tslint.json configuration. + */ + tslintConfig?: string; + /** + * File glob(s) to ignore. + */ + exclude?: (string | string[]); + }[]; + /** + * Configuration for unit tests. + */ + test?: { + karma?: { + /** + * Path to the karma config file. + */ + config?: string; + }; + codeCoverage?: { + /** + * Globs to exclude from code coverage. + */ + exclude?: string[]; + }; + }; + /** + * Specify the default values for generating. + */ + defaults?: { + /** + * The file extension to be used for style files. + */ + styleExt?: string; + /** + * How often to check for file updates. + */ + poll?: number; + /** + * Use lint to fix files after generation + */ + lintFix?: boolean; + /** + * Options for generating a class. + */ + class?: { + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a component. + */ + component?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + /** + * Specifies if the style will be in the ts file. + */ + inlineStyle?: boolean; + /** + * Specifies if the template will be in the ts file. + */ + inlineTemplate?: boolean; + /** + * Specifies the view encapsulation strategy. + */ + viewEncapsulation?: ('Emulated' | 'Native' | 'None'); + /** + * Specifies the change detection strategy. + */ + changeDetection?: ('Default' | 'OnPush'); + }; + /** + * Options for generating a directive. + */ + directive?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a guard. + */ + guard?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating an interface. + */ + interface?: { + /** + * Prefix to apply to interface names. (i.e. I) + */ + prefix?: string; + }; + /** + * Options for generating a module. + */ + module?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a pipe. + */ + pipe?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a service. + */ + service?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Properties to be passed to the build command. + */ + build?: { + /** + * Output sourcemaps. + */ + sourcemaps?: boolean; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The ssl key used by the server. + */ + progress?: boolean; + /** + * Enable and define the file watching poll time period (milliseconds). + */ + poll?: number; + /** + * Delete output path before build. + */ + deleteOutputPath?: boolean; + /** + * Do not use the real path when resolving modules. + */ + preserveSymlinks?: boolean; + /** + * Show circular dependency warnings on builds. + */ + showCircularDependencies?: boolean; + /** + * Use a separate bundle containing code used across multiple bundles. + */ + commonChunk?: boolean; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + }; + /** + * Properties to be passed to the serve command. + */ + serve?: { + /** + * The port the application will be served on. + */ + port?: number; + /** + * The host the application will be served on. + */ + host?: string; + /** + * Enables ssl for the application. + */ + ssl?: boolean; + /** + * The ssl key used by the server. + */ + sslKey?: string; + /** + * The ssl certificate used by the server. + */ + sslCert?: string; + /** + * Proxy configuration file. + */ + proxyConfig?: string; + }; + /** + * Properties about schematics. + */ + schematics?: { + /** + * The schematics collection to use. + */ + collection?: string; + /** + * The new app schematic. + */ + newApp?: string; + }; + }; + /** + * Specify which package manager tool to use. + */ + packageManager?: ('npm' | 'cnpm' | 'yarn' | 'default'); + /** + * Allow people to disable console warnings. + */ + warnings?: { + /** + * Show a warning when the user enabled the --hmr option. + */ + hmrWarning?: boolean; + /** + * Show a warning when the node version is incompatible. + */ + nodeDeprecation?: boolean; + /** + * Show a warning when the user installed angular-cli. + */ + packageDeprecation?: boolean; + /** + * Show a warning when the global version is newer than the local one. + */ + versionMismatch?: boolean; + /** + * Show a warning when the TypeScript version is incompatible + */ + typescriptMismatch?: boolean; + }; +} + +export type WorkspaceSchema = experimental.workspace.WorkspaceSchema; + + +export function getWorkspacePath(host: Tree): string { + const possibleFiles = [ '/angular.json', '/.angular.json' ]; + const path = possibleFiles.filter(path => host.exists(path))[0]; + + return path; +} + +export function getWorkspace(host: Tree): WorkspaceSchema { + const path = getWorkspacePath(host); + const configBuffer = host.read(path); + if (configBuffer === null) { + throw new SchematicsException(`Could not find (${path})`); + } + const config = configBuffer.toString(); + + return JSON.parse(config); +} + +export const configPath = '/.angular-cli.json'; + +export function getConfig(host: Tree): CliConfig { + const configBuffer = host.read(configPath); + if (configBuffer === null) { + throw new SchematicsException('Could not find .angular-cli.json'); + } + + const config = JSON.parse(configBuffer.toString()); + + return config; +} + +export function getAppFromConfig(config: CliConfig, appIndexOrName: string): AppConfig | null { + if (!config.apps) { + return null; + } + + if (parseInt(appIndexOrName) >= 0) { + return config.apps[parseInt(appIndexOrName)]; + } + + return config.apps.filter((app) => app.name === appIndexOrName)[0]; +} diff --git a/packages/angular_devkit/architect/package.json b/packages/angular_devkit/architect/package.json index 4d797be5c2..a9c0d5004b 100644 --- a/packages/angular_devkit/architect/package.json +++ b/packages/angular_devkit/architect/package.json @@ -9,6 +9,6 @@ }, "dependencies": { "@angular-devkit/core": "0.0.0", - "rxjs": "^5.5.6" + "rxjs": "^6.0.0-beta.3" } -} +} \ No newline at end of file diff --git a/packages/angular_devkit/architect/src/architect.ts b/packages/angular_devkit/architect/src/architect.ts index 55e4e48742..00d8b0981b 100644 --- a/packages/angular_devkit/architect/src/architect.ts +++ b/packages/angular_devkit/architect/src/architect.ts @@ -12,54 +12,42 @@ import { JsonParseMode, Path, dirname, + experimental, getSystemPath, join, logging, normalize, parseJson, - resolve, - schema, virtualFs, } from '@angular-devkit/core'; import { resolve as nodeResolve } from '@angular-devkit/core/node'; -import { Observable } from 'rxjs/Observable'; -import { of } from 'rxjs/observable/of'; -import { _throw } from 'rxjs/observable/throw'; -import { concatMap } from 'rxjs/operators'; +import { Observable, forkJoin, of, throwError } from 'rxjs'; +import { concatMap, map, tap } from 'rxjs/operators'; import { BuildEvent, Builder, BuilderConstructor, BuilderContext, BuilderDescription, - BuilderMap, + BuilderPaths, + BuilderPathsMap, } from './builder'; -import { Workspace } from './workspace'; - export class ProjectNotFoundException extends BaseException { - constructor(name?: string) { - const nameOrDefault = name ? `Project '${name}'` : `Default project`; - super(`${nameOrDefault} could not be found in workspace.`); + constructor(projectName: string) { + super(`Project '${projectName}' could not be found in Workspace.`); } } export class TargetNotFoundException extends BaseException { - constructor(name?: string) { - const nameOrDefault = name ? `Target '${name}'` : `Default target`; - super(`${nameOrDefault} could not be found in workspace.`); + constructor(projectName: string, targetName: string) { + super(`Target '${targetName}' could not be found in project '${projectName}'.`); } } export class ConfigurationNotFoundException extends BaseException { - constructor(name: string) { - super(`Configuration '${name}' could not be found in project.`); - } -} - -export class SchemaValidationException extends BaseException { - constructor(errors: string[]) { - super(`Schema validation failed with the following errors:\n ${errors.join('\n ')}`); + constructor(projectName: string, configurationName: string) { + super(`Configuration '${configurationName}' could not be found in project '${projectName}'.`); } } @@ -70,206 +58,284 @@ export class BuilderCannotBeResolvedException extends BaseException { } } -export class WorkspaceNotYetLoadedException extends BaseException { - constructor() { super(`Workspace needs to be loaded before Architect is used.`); } +export class ArchitectNotYetLoadedException extends BaseException { + constructor() { super(`Architect needs to be loaded before Architect is used.`); } +} + +export class BuilderNotFoundException extends BaseException { + constructor(builder: string) { + super(`Builder ${builder} could not be found.`); + } } -export interface Target { +export interface BuilderConfiguration { root: Path; projectType: string; builder: string; options: OptionsT; } -export interface TargetOptions { - project?: string; - target?: string; +export interface TargetSpecifier { + project: string; + target: string; configuration?: string; overrides?: Partial; } -export class Architect { - private readonly _workspaceSchema = join(normalize(__dirname), 'workspace-schema.json'); - private readonly _buildersSchema = join(normalize(__dirname), 'builders-schema.json'); - private _workspace: Workspace; +export interface TargetMap { + [k: string]: Target; +} - constructor(private _root: Path, private _host: virtualFs.Host<{}>) { } +export declare type TargetOptions = T; +export declare type TargetConfiguration = Partial; - loadWorkspaceFromHost(workspacePath: Path) { - return this._host.read(join(this._root, workspacePath)).pipe( - concatMap((buffer) => { - const json = JSON.parse(virtualFs.fileBufferToString(buffer)); +export interface Target { + builder: string; + options: TargetOptions; + configurations?: { [k: string]: TargetConfiguration }; +} - return this.loadWorkspaceFromJson(json); - }), - ); +export class Architect { + private readonly _targetsSchemaPath = join(normalize(__dirname), 'targets-schema.json'); + private readonly _buildersSchemaPath = join(normalize(__dirname), 'builders-schema.json'); + private _targetsSchema: JsonObject; + private _buildersSchema: JsonObject; + private _architectSchemasLoaded = false; + private _targetMapMap = new Map(); + private _builderPathsMap = new Map(); + private _builderDescriptionMap = new Map(); + private _builderConstructorMap = new Map>(); + + constructor(private _workspace: experimental.workspace.Workspace) { } + + loadArchitect() { + if (this._architectSchemasLoaded) { + return of(this); + } else { + return forkJoin( + this._loadJsonFile(this._targetsSchemaPath), + this._loadJsonFile(this._buildersSchemaPath), + ).pipe( + concatMap(([targetsSchema, buildersSchema]) => { + this._targetsSchema = targetsSchema; + this._buildersSchema = buildersSchema; + this._architectSchemasLoaded = true; + + // Validate and cache all project target maps. + return forkJoin( + ...this._workspace.listProjectNames().map(projectName => { + const unvalidatedTargetMap = this._workspace.getProjectArchitect(projectName); + + return this._workspace.validateAgainstSchema( + unvalidatedTargetMap, this._targetsSchema).pipe( + tap(targetMap => this._targetMapMap.set(projectName, targetMap)), + ); + }), + ); + }), + map(() => this), + ); + } } - loadWorkspaceFromJson(json: Workspace) { - return this._validateAgainstSchema(json, this._workspaceSchema).pipe( - concatMap((validatedWorkspace: Workspace) => { - this._workspace = validatedWorkspace; - - return of(this); - }), - ); + listProjectTargets(projectName: string): string[] { + return Object.keys(this._getProjectTargetMap(projectName)); } - getTarget(options: TargetOptions = {}): Target { - let { project, target: targetName } = options; - const { configuration, overrides } = options; - - if (!this._workspace) { - throw new WorkspaceNotYetLoadedException(); + private _getProjectTargetMap(projectName: string): TargetMap { + if (!this._targetMapMap.has(projectName)) { + throw new ProjectNotFoundException(projectName); } - project = project || this._workspace.defaultProject as string; - const workspaceProject = this._workspace.projects[project]; + return this._targetMapMap.get(projectName) as TargetMap; + } - if (!workspaceProject) { - throw new ProjectNotFoundException(project); - } + private _getProjectTarget(projectName: string, targetName: string): Target { + const targetMap = this._getProjectTargetMap(projectName); - targetName = targetName || workspaceProject.defaultTarget as string; - const workspaceTarget = workspaceProject.targets[targetName]; + const target = targetMap[targetName] as {} as Target; - if (!workspaceTarget) { - throw new TargetNotFoundException(targetName); + if (!target) { + throw new TargetNotFoundException(projectName, targetName); } - const workspaceTargetOptions = workspaceTarget.options; - let workspaceConfiguration; + return target; + } + + getBuilderConfiguration(targetSpec: TargetSpecifier): BuilderConfiguration { + const { + project: projectName, + target: targetName, + configuration: configurationName, + overrides, + } = targetSpec; + + const project = this._workspace.getProject(projectName); + const target = this._getProjectTarget(projectName, targetName); + const options = target.options; + let configuration: TargetConfiguration = {}; + + if (configurationName) { + if (!target.configurations) { + throw new ConfigurationNotFoundException(projectName, configurationName); + } - if (configuration) { - workspaceConfiguration = workspaceTarget.configurations - && workspaceTarget.configurations[configuration]; + configuration = target.configurations[configurationName]; - if (!workspaceConfiguration) { - throw new ConfigurationNotFoundException(configuration); + if (!configuration) { + throw new ConfigurationNotFoundException(projectName, configurationName); } } - // Resolve root for the target. - // TODO: add Path format to JSON schemas - const target: Target = { - root: resolve(this._root, normalize(workspaceProject.root)), - projectType: workspaceProject.projectType, - builder: workspaceTarget.builder, + const builderConfiguration: BuilderConfiguration = { + root: project.root, + projectType: project.projectType, + builder: target.builder, options: { - ...workspaceTargetOptions, - ...workspaceConfiguration, + ...options, + ...configuration, ...overrides as {}, } as OptionsT, }; - return target; + return builderConfiguration; } - // Will run the target using the target. run( - target: Target, + builderConfig: BuilderConfiguration, partialContext: Partial = {}, ): Observable { const context: BuilderContext = { logger: new logging.NullLogger(), architect: this, - host: this._host, + host: this._workspace.host, + workspace: this._workspace, ...partialContext, }; let builderDescription: BuilderDescription; - return this.getBuilderDescription(target).pipe( - concatMap(description => { - builderDescription = description; - - return this.validateBuilderOptions(target, builderDescription); - }), - concatMap(() => of(this.getBuilder(builderDescription, context))), - concatMap(builder => builder.run(target)), + return this.getBuilderDescription(builderConfig).pipe( + tap(description => builderDescription = description), + concatMap(() => this.validateBuilderOptions(builderConfig, builderDescription)), + tap(validatedBuilderConfig => builderConfig = validatedBuilderConfig), + map(() => this.getBuilder(builderDescription, context)), + concatMap(builder => builder.run(builderConfig)), ); } - getBuilderDescription(target: Target): Observable { + getBuilderDescription( + builderConfig: BuilderConfiguration, + ): Observable { + // Check cache for this builder description. + if (this._builderDescriptionMap.has(builderConfig.builder)) { + return of(this._builderDescriptionMap.get(builderConfig.builder) as BuilderDescription); + } + return new Observable((obs) => { // TODO: this probably needs to be more like NodeModulesEngineHost. - const basedir = getSystemPath(this._root); - const [pkg, builderName] = target.builder.split(':'); - const pkgJsonPath = nodeResolve(pkg, { basedir, resolvePackageJson: true }); + const basedir = getSystemPath(this._workspace.root); + const [pkg, builderName] = builderConfig.builder.split(':'); + const pkgJsonPath = nodeResolve(pkg, { basedir, resolvePackageJson: true, checkLocal: true }); let buildersJsonPath: Path; + let builderPaths: BuilderPaths; // Read the `builders` entry of package.json. - return this._host.read(normalize(pkgJsonPath)).pipe( - concatMap(buffer => - of(parseJson(virtualFs.fileBufferToString(buffer), JsonParseMode.Loose))), + return this._loadJsonFile(normalize(pkgJsonPath)).pipe( concatMap((pkgJson: JsonObject) => { const pkgJsonBuildersentry = pkgJson['builders'] as string; if (!pkgJsonBuildersentry) { - throw new BuilderCannotBeResolvedException(target.builder); + return throwError(new BuilderCannotBeResolvedException(builderConfig.builder)); } buildersJsonPath = join(dirname(normalize(pkgJsonPath)), pkgJsonBuildersentry); - return this._host.read(buildersJsonPath); + return this._loadJsonFile(buildersJsonPath); }), - concatMap((buffer) => of(JSON.parse(virtualFs.fileBufferToString(buffer)))), // Validate builders json. - concatMap((builderMap) => - this._validateAgainstSchema(builderMap, this._buildersSchema)), - + concatMap((builderPathsMap) => this._workspace.validateAgainstSchema( + builderPathsMap, this._buildersSchema)), + concatMap((builderPathsMap) => { + builderPaths = builderPathsMap.builders[builderName]; - concatMap((builderMap) => { - const builderDescription = builderMap.builders[builderName]; - - if (!builderDescription) { - throw new BuilderCannotBeResolvedException(target.builder); + if (!builderPaths) { + return throwError(new BuilderCannotBeResolvedException(builderConfig.builder)); } - // Resolve paths in the builder description. + // Resolve paths in the builder paths. const builderJsonDir = dirname(buildersJsonPath); - builderDescription.schema = join(builderJsonDir, builderDescription.schema); - builderDescription.class = join(builderJsonDir, builderDescription.class); + builderPaths.schema = join(builderJsonDir, builderPaths.schema); + builderPaths.class = join(builderJsonDir, builderPaths.class); + + // Save the builder paths so that we can lazily load the builder. + this._builderPathsMap.set(builderConfig.builder, builderPaths); - // Validate options again builder schema. - return of(builderDescription); + // Load the schema. + return this._loadJsonFile(builderPaths.schema); + }), + map(builderSchema => { + const builderDescription = { + name: builderConfig.builder, + schema: builderSchema, + description: builderPaths.description, + }; + + // Save to cache before returning. + this._builderDescriptionMap.set(builderDescription.name, builderDescription); + + return builderDescription; }), ).subscribe(obs); }); } validateBuilderOptions( - target: Target, builderDescription: BuilderDescription, - ): Observable { - return this._validateAgainstSchema(target.options, - normalize(builderDescription.schema)); + builderConfig: BuilderConfiguration, builderDescription: BuilderDescription, + ): Observable> { + return this._workspace.validateAgainstSchema( + builderConfig.options, builderDescription.schema, + ).pipe( + map(validatedOptions => { + builderConfig.options = validatedOptions; + + return builderConfig; + }), + ); } getBuilder( builderDescription: BuilderDescription, context: BuilderContext, ): Builder { - // TODO: support more than the default export, maybe via builder#import-name. - const builderModule = require(getSystemPath(builderDescription.class)); - const builderClass = builderModule['default'] as BuilderConstructor; + const name = builderDescription.name; + let builderConstructor: BuilderConstructor; + + // Check cache for this builder. + if (this._builderConstructorMap.has(name)) { + builderConstructor = this._builderConstructorMap.get(name) as BuilderConstructor; + } else { + if (!this._builderPathsMap.has(name)) { + throw new BuilderNotFoundException(name); + } + + const builderPaths = this._builderPathsMap.get(name) as BuilderPaths; - return new builderClass(context); + // TODO: support more than the default export, maybe via builder#import-name. + const builderModule = require(getSystemPath(builderPaths.class)); + builderConstructor = builderModule['default'] as BuilderConstructor; + + // Save builder to cache before returning. + this._builderConstructorMap.set(builderDescription.name, builderConstructor); + } + + const builder = new builderConstructor(context); + + return builder; } - // Warning: this method changes contentJson in place. - // TODO: add transforms to resolve paths. - private _validateAgainstSchema(contentJson: {}, schemaPath: Path): Observable { - const registry = new schema.CoreSchemaRegistry(); - - return this._host.read(schemaPath).pipe( - concatMap((buffer) => of(JSON.parse(virtualFs.fileBufferToString(buffer)))), - concatMap((schemaContent) => registry.compile(schemaContent)), - concatMap(validator => validator(contentJson)), - concatMap(validatorResult => { - if (validatorResult.success) { - return of(contentJson as T); - } else { - return _throw(new SchemaValidationException(validatorResult.errors as string[])); - } - }), + private _loadJsonFile(path: Path): Observable { + return this._workspace.host.read(normalize(path)).pipe( + map(buffer => virtualFs.fileBufferToString(buffer)), + map(str => parseJson(str, JsonParseMode.Loose) as {} as JsonObject), ); } } diff --git a/packages/angular_devkit/architect/src/architect_spec.ts b/packages/angular_devkit/architect/src/architect_spec.ts index c4f57a569b..a040a2793e 100644 --- a/packages/angular_devkit/architect/src/architect_spec.ts +++ b/packages/angular_devkit/architect/src/architect_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { join, normalize } from '@angular-devkit/core'; +import { experimental, normalize, schema } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { concatMap, tap, toArray } from 'rxjs/operators'; import { BrowserTargetOptions } from '../test/browser'; @@ -14,27 +14,23 @@ import { Architect, BuilderCannotBeResolvedException, ConfigurationNotFoundException, - ProjectNotFoundException, - Target, TargetNotFoundException, } from './architect'; -import { Workspace } from './workspace'; describe('Architect', () => { const host = new NodeJsSyncHost(); const root = normalize(__dirname); - const workspace: Workspace = { - name: 'spec', + const workspace = new experimental.workspace.Workspace(root, host); + let architect: Architect; + const workspaceJson = { version: 1, - root: 'src', - defaultProject: 'app', + newProjectRoot: 'src', projects: { app: { root: 'app', projectType: 'application', - defaultTarget: 'browser', - targets: { + architect: { browser: { builder: '../test:browser', options: { @@ -42,10 +38,16 @@ describe('Architect', () => { }, configurations: { prod: { - optimizationLevel: 1, + optionalBrowserOption: false, }, }, }, + badBrowser: { + builder: '../test:browser', + options: { + badBrowserOption: 1, + }, + }, karma: { builder: '../test:karma', options: {}, @@ -55,94 +57,41 @@ describe('Architect', () => { }, }; - it('works', (done) => { - const architect = new Architect(root, host); - architect.loadWorkspaceFromJson(workspace).subscribe({ - complete: () => { - const target = architect.getTarget(); - const options = target.options; - - // Check options were composed properly. - expect(target.root).toBe(join(root, 'app')); - expect(target.projectType).toBe('application'); - expect(target.builder).toBe('../test:browser'); - expect(options.browserOption).toBe(1); - - done(); - }, - error: done.fail, - }); + beforeAll((done) => workspace.loadWorkspaceFromJson(workspaceJson).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + tap(arch => architect = arch), + ).subscribe(undefined, done.fail, done)); + + it('works', () => { + const targetSpec = { project: 'app', target: 'browser', configuration: 'prod' }; + const builderConfig = architect.getBuilderConfiguration(targetSpec); + expect(builderConfig.root).toBe('app'); + expect(builderConfig.projectType).toBe('application'); + expect(builderConfig.builder).toBe('../test:browser'); + expect(builderConfig.options.browserOption).toBe(1); + expect(builderConfig.options.optionalBrowserOption).toBe(false); }); - it('composes project with target and configuration', (done) => { - const architect = new Architect(root, host); - const targetOptions = { - project: 'app', - target: 'browser', - configuration: 'prod', - }; - architect.loadWorkspaceFromJson(workspace).subscribe({ - complete: () => { - const target = architect.getTarget(targetOptions); - const options = target.options; - - // Check options were composed properly. - expect(target.root).toBe(join(root, 'app')); - expect(target.projectType).toBe('application'); - expect(target.builder).toBe('../test:browser'); - expect(options.browserOption).toBe(1); - expect(options.optimizationLevel).toBe(1); - - done(); - }, - error: done.fail, - }); + it('lists targets by name', () => { + expect(architect.listProjectTargets('app')).toEqual(['browser', 'badBrowser', 'karma']); }); - it('throws when missing project is used', (done) => { - const architect = new Architect(root, host); - const targetOptions = { project: 'missing' }; - architect.loadWorkspaceFromJson(workspace).subscribe({ - complete: () => { - const err = new ProjectNotFoundException('missing'); - expect(() => architect.getTarget(targetOptions)).toThrow(err); - done(); - }, - error: done.fail, - }); + it('errors when missing target is used', () => { + const targetSpec = { project: 'app', target: 'missing', configuration: 'prod' }; + expect(() => architect.getBuilderConfiguration(targetSpec)) + .toThrow(new TargetNotFoundException(targetSpec.project, targetSpec.target)); }); - it('throws when missing target is used', (done) => { - const architect = new Architect(root, host); - const targetOptions = { target: 'missing' }; - architect.loadWorkspaceFromJson(workspace).subscribe({ - complete: () => { - const err = new TargetNotFoundException('missing'); - expect(() => architect.getTarget(targetOptions)).toThrow(err); - done(); - }, - error: done.fail, - }); - }); - - it('throws when missing configuration is used', (done) => { - const architect = new Architect(root, host); - const targetOptions = { configuration: 'missing' }; - architect.loadWorkspaceFromJson(workspace).subscribe({ - complete: () => { - const err = new ConfigurationNotFoundException('missing'); - expect(() => architect.getTarget(targetOptions)).toThrow(err); - done(); - }, - error: done.fail, - }); + it('throws when missing configuration is used', () => { + const targetSpec = { project: 'app', target: 'browser', configuration: 'missing' }; + expect(() => architect.getBuilderConfiguration(targetSpec)) + .toThrow(new ConfigurationNotFoundException(targetSpec.project, targetSpec.configuration)); }); it('runs targets', (done) => { - const architect = new Architect(root, host); - const targetOptions = { project: 'app', target: 'browser' }; - architect.loadWorkspaceFromJson(workspace).pipe( - concatMap((architect) => architect.run(architect.getTarget(targetOptions))), + const targetSpec = { project: 'app', target: 'browser', configuration: 'prod' }; + const builderConfig = architect.getBuilderConfiguration(targetSpec); + architect.run(builderConfig).pipe( toArray(), tap(events => { expect(events.length).toBe(3); @@ -150,24 +99,24 @@ describe('Architect', () => { expect(events[1].success).toBe(false); expect(events[2].success).toBe(true); }), - ).subscribe(done, done.fail); - + ).subscribe(undefined, done.fail, done); }); - it('throws when invalid target is used', (done) => { - let target: Target; - const architect = new Architect(root, host); - const targetOptions = { project: 'app', target: 'karma' }; - architect.loadWorkspaceFromJson(workspace).pipe( - concatMap((architect) => { - target = architect.getTarget(targetOptions); + it('errors when builder cannot be resolved', (done) => { + const targetSpec = { project: 'app', target: 'karma' }; + const builderConfig = architect.getBuilderConfiguration(targetSpec); + architect.run(builderConfig).subscribe(undefined, (err: Error) => { + expect(err).toEqual(jasmine.any(BuilderCannotBeResolvedException)); + done(); + }, done.fail); + }); - return architect.run(target); - }), - ).subscribe(() => done.fail(), (err: Error) => { - const expectedErr = new BuilderCannotBeResolvedException(target.builder); - expect(err.message).toEqual(expectedErr.message); + it('errors when builder options fail validation', (done) => { + const targetSpec = { project: 'app', target: 'badBrowser' }; + const builderConfig = architect.getBuilderConfiguration(targetSpec); + architect.run(builderConfig).subscribe(undefined, (err: Error) => { + expect(err).toEqual(jasmine.any(schema.SchemaValidationException)); done(); - }); + }, done.fail); }); }); diff --git a/packages/angular_devkit/architect/src/builder.ts b/packages/angular_devkit/architect/src/builder.ts index ff875af14f..525e03a41e 100644 --- a/packages/angular_devkit/architect/src/builder.ts +++ b/packages/angular_devkit/architect/src/builder.ts @@ -6,14 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import { Path, logging, virtualFs } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { Architect, Target } from './architect'; +import { JsonObject, Path, experimental, logging, virtualFs } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Architect, BuilderConfiguration } from './architect'; export interface BuilderContext { logger: logging.Logger; host: virtualFs.Host<{}>; + workspace: experimental.workspace.Workspace; architect: Architect; } @@ -25,19 +26,25 @@ export interface BuildEvent { } export interface Builder { - run(_target: Target>): Observable; + run(builderConfig: BuilderConfiguration>): Observable; } -export interface BuilderMap { - builders: { [k: string]: BuilderDescription }; +export interface BuilderPathsMap { + builders: { [k: string]: BuilderPaths }; } -export interface BuilderDescription { +export interface BuilderPaths { class: Path; schema: Path; description: string; } +export interface BuilderDescription { + name: string; + schema: JsonObject; + description: string; +} + export interface BuilderConstructor { new(context: BuilderContext): Builder; } diff --git a/packages/angular_devkit/architect/src/index.ts b/packages/angular_devkit/architect/src/index.ts index 64ad39b023..44e3fe06ab 100644 --- a/packages/angular_devkit/architect/src/index.ts +++ b/packages/angular_devkit/architect/src/index.ts @@ -8,4 +8,3 @@ export * from './architect'; export * from './builder'; -export * from './workspace'; diff --git a/packages/angular_devkit/architect/src/targets-schema.json b/packages/angular_devkit/architect/src/targets-schema.json new file mode 100644 index 0000000000..b415dc4926 --- /dev/null +++ b/packages/angular_devkit/architect/src/targets-schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "ArchitectTargets", + "title": "Targets schema for validating Architect targets configuration.", + "type": "object", + "description": "A map of available project targets.", + "additionalProperties": { + "$ref": "#/definitions/target" + }, + "required": [], + "definitions": { + "target": { + "type": "object", + "description": "Target options.", + "properties": { + "builder": { + "type": "string", + "description": "The builder used for this package." + }, + "options": { + "$ref": "#/definitions/options" + }, + "configurations": { + "type": "object", + "description": "A map of alternative target options.", + "additionalProperties": { + "$ref": "#/definitions/options" + } + } + }, + "additionalProperties": false, + "required": [ + "builder", + "options" + ] + }, + "options": { + "type": "object", + "description": "Target options." + } + } +} \ No newline at end of file diff --git a/packages/angular_devkit/architect/src/workspace.ts b/packages/angular_devkit/architect/src/workspace.ts deleted file mode 100644 index 2157d24680..0000000000 --- a/packages/angular_devkit/architect/src/workspace.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 { JsonObject } from '@angular-devkit/core'; - -export interface Workspace { - name: string; - version: number; - root: string; - defaultProject?: string; - projects: { [k: string]: WorkspaceProject }; -} - -export interface WorkspaceProject { - projectType: 'application' | 'library'; - root: string; - defaultTarget?: string; - targets: { [k: string]: WorkspaceTarget }; -} - -export interface WorkspaceTarget { - builder: string; - options: TargetOptions; - configurations?: { [k: string]: Partial }; -} diff --git a/packages/angular_devkit/architect/test/browser/index.ts b/packages/angular_devkit/architect/test/browser/index.ts index fee4853274..9c553f3cc3 100644 --- a/packages/angular_devkit/architect/test/browser/index.ts +++ b/packages/angular_devkit/architect/test/browser/index.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { Observable } from 'rxjs/Observable'; -import { BuildEvent, Builder, Target } from '../../src'; +import { Observable } from 'rxjs'; +import { BuildEvent, Builder, BuilderConfiguration } from '../../src'; const successBuildEvent: BuildEvent = { @@ -20,13 +20,11 @@ const failBuildEvent: BuildEvent = { export interface BrowserTargetOptions { browserOption: number; - optimizationLevel: number; + optionalBrowserOption: boolean; } export default class BrowserTarget implements Builder { - // constructor(public context: BuilderContext) { } - - run(_info: Target>): Observable { + run(_browserConfig: BuilderConfiguration>): Observable { return new Observable(obs => { obs.next(successBuildEvent); obs.next(failBuildEvent); diff --git a/packages/angular_devkit/architect/test/browser/schema.json b/packages/angular_devkit/architect/test/browser/schema.json index 064bafe1a8..ee1dc6002b 100644 --- a/packages/angular_devkit/architect/test/browser/schema.json +++ b/packages/angular_devkit/architect/test/browser/schema.json @@ -9,7 +9,7 @@ "type": "number", "description": "A required option" }, - "optimize": { + "optionalBrowserOption": { "type": "boolean", "description": "A non-required option with a default", "default": false diff --git a/packages/angular_devkit/architect/test/package.json b/packages/angular_devkit/architect/test/package.json index 438977c186..ae8e52521e 100644 --- a/packages/angular_devkit/architect/test/package.json +++ b/packages/angular_devkit/architect/test/package.json @@ -1,6 +1,6 @@ { "builders": "builders.json", "dependencies": { - "rxjs": "^5.5.6" + "rxjs": "^6.0.0-beta.3" } } diff --git a/packages/angular_devkit/architect_cli/bin/architect.ts b/packages/angular_devkit/architect_cli/bin/architect.ts index 865bf7e45c..36aa58b7f5 100644 --- a/packages/angular_devkit/architect_cli/bin/architect.ts +++ b/packages/angular_devkit/architect_cli/bin/architect.ts @@ -7,13 +7,16 @@ * found in the LICENSE file at https://angular.io/license */ -import { Architect, Workspace } from '@angular-devkit/architect'; -import { dirname, normalize, tags } from '@angular-devkit/core'; +import 'symbol-observable'; +// symbol polyfill must go first +// tslint:disable-next-line:ordered-imports import-groups +import { Architect } from '@angular-devkit/architect'; +import { dirname, experimental, normalize, tags } from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; import { existsSync, readFileSync } from 'fs'; import * as minimist from 'minimist'; import * as path from 'path'; -import { _throw } from 'rxjs/observable/throw'; +import { throwError } from 'rxjs'; import { concatMap } from 'rxjs/operators'; @@ -81,48 +84,60 @@ if (targetStr) { // Load workspace configuration file. const currentPath = process.cwd(); -const configFileName = '.architect.json'; -const configFilePath = findUp([configFileName], currentPath); +const configFileNames = [ + 'angular.json', + '.angular.json', + 'workspace.json', + '.workspace.json', +]; + +const configFilePath = findUp(configFileNames, currentPath); if (!configFilePath) { - logger.fatal(`Workspace configuration file (${configFileName}) cannot be found in ` + logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in ` + `'${currentPath}' or in parent directories.`); process.exit(3); throw 3; // TypeScript doesn't know that process.exit() never returns. } -const workspacePath = dirname(normalize(configFilePath)); +const root = dirname(normalize(configFilePath)); const configContent = readFileSync(configFilePath, 'utf-8'); -const configJson = JSON.parse(configContent) as Workspace; +const workspaceJson = JSON.parse(configContent); const host = new NodeJsSyncHost(); -const architect = new Architect(workspacePath, host); -architect.loadWorkspaceFromJson(configJson).pipe( - concatMap(() => { +const workspace = new experimental.workspace.Workspace(root, host); + +let lastBuildEvent = { success: true }; + +workspace.loadWorkspaceFromJson(workspaceJson).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + concatMap(architect => { + const overrides = { ...argv }; delete overrides['help']; delete overrides['_']; - const targetOptions = { + const targetSpec = { project, target: targetName, configuration, overrides, }; - const target = architect.getTarget(targetOptions); // TODO: better logging of what's happening. if (argv.help) { // TODO: add target help - return _throw('Target help NYI.'); + return throwError('Target help NYI.'); // architect.help(targetOptions, logger); } else { - return architect.run(target, { logger }); + const builderConfig = architect.getBuilderConfiguration(targetSpec); + + return architect.run(builderConfig, { logger }); } }), ).subscribe({ - next: (event => logger.info(JSON.stringify(event, null, 2))), - complete: () => process.exit(0), + next: (buildEvent => lastBuildEvent = buildEvent), + complete: () => process.exit(lastBuildEvent.success ? 0 : 1), error: (err: Error) => { logger.fatal(err.message); if (err.stack) { diff --git a/packages/angular_devkit/architect_cli/package.json b/packages/angular_devkit/architect_cli/package.json index c2162242d3..16ffb9972e 100644 --- a/packages/angular_devkit/architect_cli/package.json +++ b/packages/angular_devkit/architect_cli/package.json @@ -18,6 +18,7 @@ "@angular-devkit/core": "0.0.0", "@angular-devkit/architect": "0.0.0", "minimist": "^1.2.0", - "rxjs": "^5.5.2" + "symbol-observable": "^1.2.0", + "rxjs": "^6.0.0-beta.3" } } diff --git a/packages/angular_devkit/build_webpack/README.md b/packages/angular_devkit/build_angular/README.md similarity index 100% rename from packages/angular_devkit/build_webpack/README.md rename to packages/angular_devkit/build_angular/README.md diff --git a/packages/angular_devkit/build_webpack/builders.json b/packages/angular_devkit/build_angular/builders.json similarity index 69% rename from packages/angular_devkit/build_webpack/builders.json rename to packages/angular_devkit/build_angular/builders.json index 244d219724..bf8b1a6ca2 100644 --- a/packages/angular_devkit/build_webpack/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -1,17 +1,22 @@ { "$schema": "../architect/src/builders-schema.json", "builders": { + "app-shell": { + "class": "./src/app-shell", + "schema": "./src/app-shell/schema.json", + "description": "Build a server app and a browser app, then render the index.html and use it for the browser output." + }, "browser": { "class": "./src/browser", "schema": "./src/browser/schema.json", "description": "Build a browser app." }, - "devServer": { + "dev-server": { "class": "./src/dev-server", "schema": "./src/dev-server/schema.json", "description": "Serve a browser app." }, - "extractI18n": { + "extract-i18n": { "class": "./src/extract-i18n", "schema": "./src/extract-i18n/schema.json", "description": "Extract i18n strings from a browser app." @@ -30,6 +35,11 @@ "class": "./src/tslint", "schema": "./src/tslint/schema.json", "description": "Run tslint over a TS project." + }, + "server": { + "class": "./src/server", + "schema": "./src/server/schema.json", + "description": "Build a server Angular application." } } } diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json new file mode 100644 index 0000000000..2f4555d536 --- /dev/null +++ b/packages/angular_devkit/build_angular/package.json @@ -0,0 +1,65 @@ +{ + "name": "@angular-devkit/build-angular", + "version": "0.0.0", + "description": "Angular Webpack Build Facade", + "main": "src/index.js", + "typings": "src/index.d.ts", + "builders": "builders.json", + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "dependencies": { + "@angular-devkit/architect": "0.0.0", + "@angular-devkit/build-optimizer": "0.0.0", + "@angular-devkit/core": "0.0.0", + "@ngtools/webpack": "0.0.0", + "ajv": "^6.0.0", + "autoprefixer": "^8.1.0", + "cache-loader": "^1.2.2", + "chalk": "~2.2.2", + "circular-dependency-plugin": "^5.0.0", + "clean-css": "^4.1.11", + "copy-webpack-plugin": "^4.5.0", + "file-loader": "^1.1.11", + "glob": "^7.0.3", + "html-webpack-plugin": "^3.0.6", + "istanbul": "^0.4.5", + "istanbul-instrumenter-loader": "^3.0.1", + "karma-source-map-support": "^1.2.0", + "less": "^3.0.1", + "less-loader": "^4.1.0", + "license-webpack-plugin": "^1.2.3", + "lodash": "^4.17.4", + "memory-fs": "^0.4.1", + "mini-css-extract-plugin": "~0.3.0", + "minimatch": "^3.0.4", + "node-sass": "^4.7.2", + "parse5": "^4.0.0", + "opn": "^5.1.0", + "portfinder": "^1.0.13", + "postcss": "^6.0.19", + "postcss-import": "^11.1.0", + "postcss-loader": "^2.1.1", + "postcss-url": "^7.3.1", + "raw-loader": "^0.5.1", + "request": "^2.83.0", + "resolve": "^1.5.0", + "rxjs": "^6.0.0-beta.3", + "sass-loader": "^6.0.7", + "silent-error": "^1.1.0", + "source-map-support": "^0.5.0", + "stats-webpack-plugin": "^0.6.2", + "style-loader": "^0.20.2", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.2", + "tree-kill": "^1.2.0", + "uglifyjs-webpack-plugin": "^1.2.2", + "url-loader": "^1.0.1", + "webpack": "~4.5.0", + "webpack-dev-middleware": "^3.1.0", + "webpack-dev-server": "^3.1.1", + "webpack-merge": "^4.1.2", + "webpack-sources": "^1.1.0", + "webpack-subresource-integrity": "^1.1.0-rc.4" + } +} \ No newline at end of file diff --git a/packages/angular_devkit/build_webpack/plugins/karma.ts b/packages/angular_devkit/build_angular/plugins/karma.ts similarity index 100% rename from packages/angular_devkit/build_webpack/plugins/karma.ts rename to packages/angular_devkit/build_angular/plugins/karma.ts diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts similarity index 95% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts index 3887d4727a..024aa9110e 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin.ts @@ -29,7 +29,7 @@ export class BaseHrefWebpackPlugin { } else { // Replace only href attribute if exists const modifiedBaseTag = baseTagMatches[0].replace( - /href="\S+"/i, `href="${this.options.baseHref}"` + /href="\S*?"/i, `href="${this.options.baseHref}"` ); htmlPluginData.html = htmlPluginData.html.replace(baseTagRegex, modifiedBaseTag); } diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts similarity index 86% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts index 2766f3b0a8..8501239e3e 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/base-href-webpack-plugin_spec.ts @@ -1,7 +1,7 @@ // tslint:disable // TODO: cleanup this file, it's copied as is from Angular CLI. -import {oneLineTrim} from 'common-tags'; +import { tags } from '@angular-devkit/core'; import {BaseHrefWebpackPlugin} from './base-href-webpack-plugin'; @@ -22,7 +22,7 @@ function mockCompiler(indexHtml: string, callback: Function) { } describe('base href webpack plugin', () => { - const html = oneLineTrim` + const html = tags.oneLine` @@ -41,7 +41,7 @@ describe('base href webpack plugin', () => { it('should insert base tag when not exist', function () { const plugin = new BaseHrefWebpackPlugin({ baseHref: '/' }); const compiler = mockCompiler(html, (_x: any, htmlPluginData: any) => { - expect(htmlPluginData.html).toEqual(oneLineTrim` + expect(htmlPluginData.html).toEqual(tags.oneLine` @@ -55,11 +55,11 @@ describe('base href webpack plugin', () => { it('should replace href attribute when base tag already exists', function () { const plugin = new BaseHrefWebpackPlugin({ baseHref: '/myUrl/' }); - const compiler = mockCompiler(oneLineTrim` + const compiler = mockCompiler(tags.oneLine` `, (_x: any, htmlPluginData: any) => { - expect(htmlPluginData.html).toEqual(oneLineTrim` + expect(htmlPluginData.html).toEqual(tags.oneLine` `); @@ -70,11 +70,11 @@ describe('base href webpack plugin', () => { it('should replace href attribute when baseHref is empty', function () { const plugin = new BaseHrefWebpackPlugin({ baseHref: '' }); - const compiler = mockCompiler(oneLineTrim` + const compiler = mockCompiler(tags.oneLine` `, (_x: any, htmlPluginData: any) => { - expect(htmlPluginData.html).toEqual(oneLineTrim` + expect(htmlPluginData.html).toEqual(tags.oneLine` `); diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/index.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/index.ts similarity index 100% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/lib/base-href-webpack/index.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/lib/base-href-webpack/index.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts new file mode 100644 index 0000000000..de7fc607d6 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// TODO: cleanup this file, it's copied as is from Angular CLI. + +// tslint:disable-next-line:no-implicit-dependencies +import * as ts from 'typescript'; +import { AssetPattern, Budget, ExtraEntryPoint } from '../../browser/schema'; + +export interface BuildOptions { + optimization: boolean; + environment?: string; + outputPath: string; + aot?: boolean; + sourceMap?: boolean; + evalSourceMap?: boolean; + vendorChunk?: boolean; + commonChunk?: boolean; + baseHref?: string; + deployUrl?: string; + verbose?: boolean; + progress?: boolean; + i18nFile?: string; + i18nFormat?: string; + i18nLocale?: string; + i18nMissingTranslation?: string; + extractCss?: boolean; + bundleDependencies?: 'none' | 'all'; + watch?: boolean; + outputHashing?: string; + poll?: number; + app?: string; + deleteOutputPath?: boolean; + preserveSymlinks?: boolean; + extractLicenses?: boolean; + showCircularDependencies?: boolean; + buildOptimizer?: boolean; + namedChunks?: boolean; + subresourceIntegrity?: boolean; + serviceWorker?: boolean; + skipAppShell?: boolean; + statsJson: boolean; + forkTypeChecker: boolean; + + main: string; + index: string; + polyfills?: string; + budgets: Budget[]; + assets: AssetPattern[]; + scripts: ExtraEntryPoint[]; + styles: ExtraEntryPoint[]; + stylePreprocessorOptions?: { includePaths: string[] }; + lazyModules: string[]; + platform?: 'browser' | 'server'; +} + +export interface WebpackTestOptions extends BuildOptions { + codeCoverage?: boolean; + codeCoverageExclude?: string[]; +} + +export interface WebpackConfigOptions { + root: string; + projectRoot: string; + buildOptions: T; + tsConfig: ts.ParsedCommandLine; + tsConfigPath: string; + supportES2015: boolean; +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts new file mode 100644 index 0000000000..bf262f9c5c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts @@ -0,0 +1,128 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +import * as path from 'path'; +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); +import { LicenseWebpackPlugin } from 'license-webpack-plugin'; +import { generateEntryPoints, packageChunkSort } from '../../utilities/package-chunk-sort'; +import { BaseHrefWebpackPlugin } from '../../lib/base-href-webpack'; +import { IndexHtmlWebpackPlugin } from '../../plugins/index-html-webpack-plugin'; +import { ExtraEntryPoint } from '../../../browser/schema'; +import { BrowserBuilderSchema } from '../../../browser/schema'; +import { WebpackConfigOptions } from '../build-options'; +import { normalizeExtraEntryPoints } from './utils'; + +/** ++ * license-webpack-plugin has a peer dependency on webpack-sources, list it in a comment to ++ * let the dependency validator know it is used. ++ * ++ * require('webpack-sources') ++ */ + +export function getBrowserConfig(wco: WebpackConfigOptions) { + const { root, projectRoot, buildOptions } = wco; + + + let extraPlugins: any[] = []; + + // Figure out which are the lazy loaded bundle names. + const lazyChunkBundleNames = normalizeExtraEntryPoints( + // We don't really need a default name because we pre-filtered by lazy only entries. + [...buildOptions.styles, ...buildOptions.scripts], 'not-lazy') + .filter(entry => entry.lazy) + .map(entry => entry.bundleName) + + const generateIndexHtml = false; + if (generateIndexHtml) { + extraPlugins.push(new HtmlWebpackPlugin({ + template: path.resolve(root, buildOptions.index), + filename: path.resolve(buildOptions.outputPath, buildOptions.index), + chunksSortMode: packageChunkSort(buildOptions), + excludeChunks: lazyChunkBundleNames, + xhtml: true, + minify: buildOptions.optimization ? { + caseSensitive: true, + collapseWhitespace: true, + keepClosingSlash: true + } : false + })); + extraPlugins.push(new BaseHrefWebpackPlugin({ + baseHref: buildOptions.baseHref as string + })); + } + + let sourcemaps: string | false = false; + if (buildOptions.sourceMap) { + // See https://webpack.js.org/configuration/devtool/ for sourcemap types. + if (buildOptions.evalSourceMap && !buildOptions.optimization) { + // Produce eval sourcemaps for development with serve, which are faster. + sourcemaps = 'eval'; + } else { + // Produce full separate sourcemaps for production. + sourcemaps = 'source-map'; + } + } + + if (buildOptions.subresourceIntegrity) { + extraPlugins.push(new SubresourceIntegrityPlugin({ + hashFuncNames: ['sha384'] + })); + } + + if (buildOptions.extractLicenses) { + extraPlugins.push(new LicenseWebpackPlugin({ + pattern: /.*/, + suppressErrors: true, + perChunkOutput: false, + outputFilename: `3rdpartylicenses.txt` + })); + } + + const globalStylesBundleNames = normalizeExtraEntryPoints(buildOptions.styles, 'styles') + .map(style => style.bundleName); + + return { + devtool: sourcemaps, + resolve: { + mainFields: [ + ...(wco.supportES2015 ? ['es2015'] : []), + 'browser', 'module', 'main' + ] + }, + output: { + crossOriginLoading: buildOptions.subresourceIntegrity ? 'anonymous' : false + }, + optimization: { + runtimeChunk: 'single', + splitChunks: { + chunks: buildOptions.commonChunk ? 'all' : 'initial', + maxAsyncRequests: Infinity, + cacheGroups: { + vendors: false, + vendor: buildOptions.vendorChunk && { + name: 'vendor', + chunks: 'initial', + enforce: true, + test: (module: any, chunks: Array<{ name: string }>) => { + const moduleName = module.nameForCondition ? module.nameForCondition() : ''; + return /[\\/]node_modules[\\/]/.test(moduleName) + && !chunks.some(({ name }) => name === 'polyfills' + || globalStylesBundleNames.includes(name)); + }, + }, + } + } + }, + plugins: extraPlugins.concat([ + new IndexHtmlWebpackPlugin({ + input: path.resolve(root, buildOptions.index), + output: path.basename(buildOptions.index), + baseHref: buildOptions.baseHref, + entrypoints: generateEntryPoints(buildOptions), + deployUrl: buildOptions.deployUrl, + }), + ]), + node: false, + }; +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts new file mode 100644 index 0000000000..5e22fd11f1 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -0,0 +1,304 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +import * as path from 'path'; +import { HashedModuleIdsPlugin } from 'webpack'; +import * as CopyWebpackPlugin from 'copy-webpack-plugin'; +import { getOutputHashFormat } from './utils'; +import { isDirectory } from '../../utilities/is-directory'; +import { requireProjectModule } from '../../utilities/require-project-module'; +import { WebpackConfigOptions } from '../build-options'; +import { BundleBudgetPlugin } from '../../plugins/bundle-budget'; +import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin'; +import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; +import { findUp } from '../../utilities/find-up'; +import { AssetPattern, ExtraEntryPoint } from '../../../browser/schema'; +import { normalizeExtraEntryPoints } from './utils'; + +const ProgressPlugin = require('webpack/lib/ProgressPlugin'); +const CircularDependencyPlugin = require('circular-dependency-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const StatsPlugin = require('stats-webpack-plugin'); +const SilentError = require('silent-error'); +const resolve = require('resolve'); + +/** + * Enumerate loaders and their dependencies from this file to let the dependency validator + * know they are used. + * + * require('source-map-loader') + * require('raw-loader') + * require('url-loader') + * require('file-loader') + * require('cache-loader') + * require('@angular-devkit/build-optimizer') + */ + +const g: any = typeof global !== 'undefined' ? global : {}; +export const buildOptimizerLoader: string = g['_DevKitIsLocal'] + ? require.resolve('@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader') + : '@angular-devkit/build-optimizer/webpack-loader'; + +export function getCommonConfig(wco: WebpackConfigOptions) { + const { root, projectRoot, buildOptions } = wco; + + const nodeModules = findUp('node_modules', projectRoot); + if (!nodeModules) { + throw new Error('Cannot locate node_modules directory.') + } + + let extraPlugins: any[] = []; + let entryPoints: { [key: string]: string[] } = {}; + + if (buildOptions.main) { + entryPoints['main'] = [path.resolve(root, buildOptions.main)]; + } + + if (buildOptions.polyfills) { + entryPoints['polyfills'] = [path.resolve(root, buildOptions.polyfills)]; + } + + // determine hashing format + const hashFormat = getOutputHashFormat(buildOptions.outputHashing as any); + + // process global scripts + if (buildOptions.scripts.length > 0) { + const globalScriptsByBundleName = normalizeExtraEntryPoints(buildOptions.scripts, 'scripts') + .reduce((prev: { bundleName: string, paths: string[], lazy: boolean }[], curr) => { + const bundleName = curr.bundleName; + const resolvedPath = path.resolve(root, curr.input); + let existingEntry = prev.find((el) => el.bundleName === bundleName); + if (existingEntry) { + if (existingEntry.lazy && !curr.lazy) { + // All entries have to be lazy for the bundle to be lazy. + throw new Error(`The ${curr.bundleName} bundle is mixing lazy and non-lazy scripts.`); + } + + existingEntry.paths.push(resolvedPath); + + } else { + prev.push({ + bundleName, + paths: [resolvedPath], + lazy: curr.lazy + }); + } + return prev; + }, []); + + + // Add a new asset for each entry. + globalScriptsByBundleName.forEach((script) => { + // Lazy scripts don't get a hash, otherwise they can't be loaded by name. + const hash = script.lazy ? '' : hashFormat.script; + const bundleName = script.bundleName; + + extraPlugins.push(new ScriptsWebpackPlugin({ + name: bundleName, + sourceMap: buildOptions.sourceMap, + filename: `${path.basename(bundleName)}${hash}.js`, + scripts: script.paths, + basePath: projectRoot, + })); + }); + } + + // process asset entries + if (buildOptions.assets) { + const copyWebpackPluginPatterns = buildOptions.assets.map((asset: AssetPattern) => { + + // Resolve input paths relative to workspace root and add slash at the end. + asset.input = path.resolve(root, asset.input).replace(/\\/g, '/'); + asset.input = asset.input.endsWith('/') ? asset.input : asset.input + '/'; + asset.output = asset.output.endsWith('/') ? asset.output : asset.output + '/'; + + if (asset.output.startsWith('..')) { + const message = 'An asset cannot be written to a location outside of the . ' + + 'You can override this message by setting the `allowOutsideOutDir` ' + + 'property on the asset to true in the CLI configuration.'; + throw new Error(message); + } + + if (asset.output.startsWith('/')) { + // Now we remove starting slash to make Webpack place it from the output root. + asset.output = asset.output.slice(1); + } + + return { + context: asset.input, + to: asset.output, + from: { + glob: asset.glob, + dot: true + } + }; + }); + + const copyWebpackPluginOptions = { ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'] }; + + const copyWebpackPluginInstance = new CopyWebpackPlugin(copyWebpackPluginPatterns, + copyWebpackPluginOptions); + + // Save options so we can use them in eject. + (copyWebpackPluginInstance as any)['copyWebpackPluginPatterns'] = copyWebpackPluginPatterns; + (copyWebpackPluginInstance as any)['copyWebpackPluginOptions'] = copyWebpackPluginOptions; + + extraPlugins.push(copyWebpackPluginInstance); + } + + if (buildOptions.progress) { + extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose, colors: true })); + } + + if (buildOptions.showCircularDependencies) { + extraPlugins.push(new CircularDependencyPlugin({ + exclude: /[\\\/]node_modules[\\\/]/ + })); + } + + if (buildOptions.statsJson) { + extraPlugins.push(new StatsPlugin('stats.json', 'verbose')); + } + + let buildOptimizerUseRule; + if (buildOptions.buildOptimizer) { + // Set the cache directory to the Build Optimizer dir, so that package updates will delete it. + const buildOptimizerDir = g['_DevKitIsLocal'] + ? nodeModules + : path.dirname(resolve.sync('@angular-devkit/build-optimizer', { basedir: projectRoot })); + const cacheDirectory = path.resolve(buildOptimizerDir, './.cache/'); + + buildOptimizerUseRule = { + use: [ + { + loader: 'cache-loader', + options: { cacheDirectory } + }, + { + loader: buildOptimizerLoader, + options: { sourceMap: buildOptions.sourceMap } + }, + ], + }; + } + + // Allow loaders to be in a node_modules nested inside the devkit/build-angular package. + // This is important in case loaders do not get hoisted. + // If this file moves to another location, alter potentialNodeModules as well. + const loaderNodeModules = ['node_modules']; + const buildAngularNodeModules = findUp('node_modules', __dirname); + if (buildAngularNodeModules + && isDirectory(buildAngularNodeModules) + && buildAngularNodeModules !== nodeModules + && buildAngularNodeModules.startsWith(nodeModules) + ) { + loaderNodeModules.push(buildAngularNodeModules); + } + + // Load rxjs path aliases. + // https://github.com/ReactiveX/rxjs/blob/master/doc/lettable-operators.md#build-and-treeshaking + let alias = {}; + try { + const rxjsPathMappingImport = wco.supportES2015 + ? 'rxjs/_esm2015/path-mapping' + : 'rxjs/_esm5/path-mapping'; + const rxPaths = requireProjectModule(projectRoot, rxjsPathMappingImport); + alias = rxPaths(nodeModules); + } catch (e) { } + + return { + mode: buildOptions.optimization ? 'production' : 'development', + devtool: false, + resolve: { + extensions: ['.ts', '.js'], + symlinks: !buildOptions.preserveSymlinks, + modules: [ + wco.tsConfig.options.baseUrl || projectRoot, + 'node_modules', + ], + alias + }, + resolveLoader: { + modules: loaderNodeModules + }, + context: projectRoot, + entry: entryPoints, + output: { + path: path.resolve(root, buildOptions.outputPath as string), + publicPath: buildOptions.deployUrl, + filename: `[name]${hashFormat.chunk}.js`, + }, + performance: { + hints: false, + }, + module: { + rules: [ + { test: /\.html$/, loader: 'raw-loader' }, + { + test: /\.(eot|svg|cur)$/, + loader: 'file-loader', + options: { + name: `[name]${hashFormat.file}.[ext]`, + limit: 10000 + } + }, + { + test: /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, + loader: 'url-loader', + options: { + name: `[name]${hashFormat.file}.[ext]`, + limit: 10000 + } + }, + { + // Mark files inside `@angular/core` as using SystemJS style dynamic imports. + // Removing this will cause deprecation warnings to appear. + test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, + parser: { system: true }, + }, + { + test: /\.js$/, + ...buildOptimizerUseRule, + }, + ] + }, + optimization: { + noEmitOnErrors: true, + minimizer: [ + new HashedModuleIdsPlugin(), + // TODO: check with Mike what this feature needs. + new BundleBudgetPlugin({ budgets: buildOptions.budgets }), + new CleanCssWebpackPlugin({ + sourceMap: buildOptions.sourceMap, + // component styles retain their original file name + test: (file) => /\.(?:css|scss|sass|less|styl)$/.test(file), + }), + new UglifyJSPlugin({ + sourceMap: buildOptions.sourceMap, + parallel: true, + cache: true, + uglifyOptions: { + ecma: wco.supportES2015 ? 6 : 5, + warnings: buildOptions.verbose, + safari10: true, + compress: { + pure_getters: buildOptions.buildOptimizer, + // PURE comments work best with 3 passes. + // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. + passes: buildOptions.buildOptimizer ? 3 : 1, + // Workaround known uglify-es issue + // See https://github.com/mishoo/UglifyJS2/issues/2949#issuecomment-368070307 + inline: wco.supportES2015 ? 1 : 3, + }, + output: { + ascii_only: true, + comments: false, + webkit: true, + }, + } + }), + ], + }, + plugins: extraPlugins, + }; +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/index.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts similarity index 81% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/index.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts index 4e42632183..ec39638e21 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/index.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/index.ts @@ -3,8 +3,6 @@ export * from './browser'; export * from './common'; -export * from './development'; -export * from './production'; export * from './server'; export * from './styles'; export * from './test'; diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/server.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts similarity index 100% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/server.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts similarity index 58% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/styles.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index 7811292dda..9ed1c8ff06 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -3,27 +3,26 @@ import * as webpack from 'webpack'; import * as path from 'path'; -import { - SuppressExtractedTextChunksWebpackPlugin -} from '../../plugins/suppress-entry-chunks-webpack-plugin'; -import { extraEntryParser, getOutputHashFormat } from './utils'; +import { SuppressExtractedTextChunksWebpackPlugin } from '../../plugins/webpack'; +import { getOutputHashFormat } from './utils'; import { WebpackConfigOptions } from '../build-options'; -// import { pluginArgs, postcssArgs } from '../../tasks/eject'; -import { CleanCssWebpackPlugin } from '../../plugins/cleancss-webpack-plugin'; +import { findUp } from '../../utilities/find-up'; +import { RawCssLoader } from '../../plugins/webpack'; +import { ExtraEntryPoint } from '../../../browser/schema'; +import { normalizeExtraEntryPoints } from './utils'; const postcssUrl = require('postcss-url'); const autoprefixer = require('autoprefixer'); -const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const postcssImports = require('postcss-import'); +const PostcssCliResources = require('../../plugins/webpack').PostcssCliResources; /** * Enumerate loaders and their dependencies from this file to let the dependency validator * know they are used. * - * require('exports-loader') * require('style-loader') * require('postcss-loader') - * require('css-loader') * require('stylus') * require('stylus-loader') * require('less') @@ -39,37 +38,48 @@ interface PostcssUrlAsset { } export function getStylesConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; + const { root, projectRoot, buildOptions } = wco; - const appRoot = path.resolve(projectRoot, appConfig.root); + // const appRoot = path.resolve(projectRoot, appConfig.root); const entryPoints: { [key: string]: string[] } = {}; const globalStylePaths: string[] = []; const extraPlugins: any[] = []; - const cssSourceMap = buildOptions.sourcemaps; + const cssSourceMap = buildOptions.sourceMap; // Maximum resource size to inline (KiB) const maximumInlineSize = 10; - // Minify/optimize css in production. - const minimizeCss = buildOptions.target === 'production'; + // Determine hashing format. + const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); // Convert absolute resource URLs to account for base-href and deploy-url. const baseHref = wco.buildOptions.baseHref || ''; const deployUrl = wco.buildOptions.deployUrl || ''; - const postcssPluginCreator = function(loader: webpack.loader.LoaderContext) { + const postcssPluginCreator = function (loader: webpack.loader.LoaderContext) { return [ postcssImports({ resolve: (url: string, context: string) => { return new Promise((resolve, reject) => { + let hadTilde = false; if (url && url.startsWith('~')) { url = url.substr(1); + hadTilde = true; } - loader.resolve(context, url, (err: Error, result: string) => { + loader.resolve(context, (hadTilde ? '' : './') + url, (err: Error, result: string) => { if (err) { - reject(err); - return; + if (hadTilde) { + reject(err); + return; + } + loader.resolve(context, url, (err: Error, result: string) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } else { + resolve(result); } - - resolve(result); }); }); }, @@ -90,7 +100,12 @@ export function getStylesConfig(wco: WebpackConfigOptions) { postcssUrl({ filter: ({ url }: PostcssUrlAsset) => url.startsWith('~'), url: ({ url }: PostcssUrlAsset) => { - const fullPath = path.join(projectRoot, 'node_modules', url.substr(1)); + // Note: This will only find the first node_modules folder. + const nodeModules = findUp('node_modules', projectRoot); + if (!nodeModules) { + throw new Error('Cannot locate node_modules directory.') + } + const fullPath = path.join(nodeModules, url.substr(1)); return path.relative(loader.context, fullPath).replace(/\\/g, '/'); } }), @@ -105,7 +120,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { } else if (baseHref.match(/:\/\//)) { // If baseHref contains a scheme, include it as is. return baseHref.replace(/\/$/, '') + - `/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); + `/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); } else { // Join together base-href, deploy-url and the original URL. // Also dedupe multiple slashes into single ones. @@ -125,53 +140,52 @@ export function getStylesConfig(wco: WebpackConfigOptions) { }, { url: 'rebase' }, ]), - autoprefixer(), + PostcssCliResources({ + deployUrl: loader.loaders[loader.loaderIndex].options.ident == 'extracted' ? '' : deployUrl, + loader, + filename: `[name]${hashFormat.file}.[ext]`, + }), + autoprefixer({ grid: true }), ]; }; - // (postcssPluginCreator as any)[postcssArgs] = { - // variableImports: { - // 'autoprefixer': 'autoprefixer', - // 'postcss-url': 'postcssUrl', - // 'postcss-import': 'postcssImports', - // }, - // variables: { minimizeCss, baseHref, deployUrl, projectRoot, maximumInlineSize } - // }; - - // determine hashing format - const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); // use includePaths from appConfig const includePaths: string[] = []; - let lessPathOptions: { paths: string[] } = {paths: []}; + let lessPathOptions: { paths: string[] } = { paths: [] }; - if (appConfig.stylePreprocessorOptions - && appConfig.stylePreprocessorOptions.includePaths - && appConfig.stylePreprocessorOptions.includePaths.length > 0 + if (buildOptions.stylePreprocessorOptions + && buildOptions.stylePreprocessorOptions.includePaths + && buildOptions.stylePreprocessorOptions.includePaths.length > 0 ) { - appConfig.stylePreprocessorOptions.includePaths.forEach((includePath: string) => - includePaths.push(path.resolve(appRoot, includePath))); + buildOptions.stylePreprocessorOptions.includePaths.forEach((includePath: string) => + includePaths.push(path.resolve(root, includePath))); lessPathOptions = { paths: includePaths, }; } - // process global styles - if (appConfig.styles.length > 0) { - const globalStyles = extraEntryParser(appConfig.styles, appRoot, 'styles'); - // add style entry points - globalStyles.forEach(style => - entryPoints[style.entry as any] - ? entryPoints[style.entry as any].push(style.path as string) - : entryPoints[style.entry as any] = [style.path as any] - ); - // add global css paths - globalStylePaths.push(...globalStyles.map((style) => style.path as any)); + // Process global styles. + if (buildOptions.styles.length > 0) { + normalizeExtraEntryPoints(buildOptions.styles, 'styles').forEach(style => { + const resolvedPath = path.resolve(root, style.input); + + // Add style entry points. + if (entryPoints[style.bundleName]) { + entryPoints[style.bundleName].push(resolvedPath) + } else { + entryPoints[style.bundleName] = [resolvedPath] + } + + // Add global css paths. + globalStylePaths.push(resolvedPath); + }); } // set base rules to derive final rules from const baseRules: webpack.NewUseRule[] = [ { test: /\.css$/, use: [] }, - { test: /\.scss$|\.sass$/, use: [{ + { + test: /\.scss$|\.sass$/, use: [{ loader: 'sass-loader', options: { sourceMap: cssSourceMap, @@ -181,7 +195,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { } }] }, - { test: /\.less$/, use: [{ + { + test: /\.less$/, use: [{ loader: 'less-loader', options: { sourceMap: cssSourceMap, @@ -200,40 +215,36 @@ export function getStylesConfig(wco: WebpackConfigOptions) { } ]; - const commonLoaders: webpack.Loader[] = [ - { - loader: 'css-loader', - options: { - sourceMap: cssSourceMap, - import: false, - } - }, - { - loader: 'postcss-loader', - options: { - // A non-function property is required to workaround a webpack option handling bug - ident: 'postcss', - plugins: postcssPluginCreator, - sourceMap: cssSourceMap - } - } - ]; - // load component css as raw strings - const rules: webpack.Rule[] = baseRules.map(({test, use}) => ({ + const rules: webpack.Rule[] = baseRules.map(({ test, use }) => ({ exclude: globalStylePaths, test, use: [ - 'exports-loader?module.exports.toString()', - ...commonLoaders, + { loader: 'raw-loader' }, + { + loader: 'postcss-loader', + options: { + ident: 'embedded', + plugins: postcssPluginCreator, + sourceMap: cssSourceMap + } + }, ...(use as webpack.Loader[]) ] })); // load global css as css files if (globalStylePaths.length > 0) { - rules.push(...baseRules.map(({test, use}) => { + rules.push(...baseRules.map(({ test, use }) => { const extractTextPlugin = { use: [ - ...commonLoaders, + { loader: RawCssLoader }, + { + loader: 'postcss-loader', + options: { + ident: buildOptions.extractCss ? 'extracted' : 'embedded', + plugins: postcssPluginCreator, + sourceMap: cssSourceMap + } + }, ...(use as webpack.Loader[]) ], // publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035 @@ -242,8 +253,10 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const ret: any = { include: globalStylePaths, test, -        use: buildOptions.extractCss ? ExtractTextPlugin.extract(extractTextPlugin) - : ['style-loader', ...extractTextPlugin.use] + use: [ + buildOptions.extractCss ? MiniCssExtractPlugin.loader : 'style-loader', + ...extractTextPlugin.use, + ] }; // Save the original options as arguments for eject. // if (buildOptions.extractCss) { @@ -256,16 +269,14 @@ export function getStylesConfig(wco: WebpackConfigOptions) { if (buildOptions.extractCss) { // extract global css from js files into own css file extraPlugins.push( - new ExtractTextPlugin({ filename: `[name]${hashFormat.extract}.bundle.css` })); + new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` })); // suppress empty .js files in css only entry points extraPlugins.push(new SuppressExtractedTextChunksWebpackPlugin()); } - if (minimizeCss) { - extraPlugins.push(new CleanCssWebpackPlugin({ sourceMap: cssSourceMap })); - } - return { + // Workaround stylus-loader defect: https://github.com/shama/stylus-loader/issues/189 + loader: { stylus: {} }, entry: entryPoints, module: { rules }, plugins: [].concat(extraPlugins as any) diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/test.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts similarity index 60% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/test.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts index f4449f08d9..a61c4f93d4 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/test.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts @@ -3,7 +3,6 @@ import * as path from 'path'; import * as glob from 'glob'; -import * as webpack from 'webpack'; // import { CliConfig } from '../config'; import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; @@ -19,16 +18,14 @@ import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; export function getTestConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; + const { root, buildOptions } = wco; - const nodeModules = path.resolve(projectRoot, 'node_modules'); const extraRules: any[] = []; const extraPlugins: any[] = []; // if (buildOptions.codeCoverage && CliConfig.fromProject()) { if (buildOptions.codeCoverage) { - // const codeCoverageExclude = CliConfig.fromProject().get('test.codeCoverage.exclude'); - const codeCoverageExclude: string[] = []; + const codeCoverageExclude = buildOptions.codeCoverageExclude; let exclude: (string | RegExp)[] = [ /\.(e2e|spec)\.ts$/, /node_modules/ @@ -37,7 +34,7 @@ export function getTestConfig(wco: WebpackConfigOptions) { if (codeCoverageExclude) { codeCoverageExclude.forEach((excludeGlob: string) => { const excludeFiles = glob - .sync(path.join(projectRoot, excludeGlob), { nodir: true }) + .sync(path.join(root, excludeGlob), { nodir: true }) .map(file => path.normalize(file)); exclude.push(...excludeFiles); }); @@ -52,29 +49,38 @@ export function getTestConfig(wco: WebpackConfigOptions) { } return { + mode: 'development', resolve: { mainFields: [ ...(wco.supportES2015 ? ['es2015'] : []), 'browser', 'module', 'main' ] }, - devtool: buildOptions.sourcemaps ? 'inline-source-map' : 'eval', + devtool: buildOptions.sourceMap ? 'inline-source-map' : 'eval', entry: { - main: path.resolve(projectRoot, appConfig.root, appConfig.main) + main: path.resolve(root, buildOptions.main) }, module: { rules: [].concat(extraRules as any) }, - plugins: [ - new webpack.optimize.CommonsChunkPlugin({ - minChunks: Infinity, - name: 'inline' - }), - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - chunks: ['main'], - minChunks: (module: any) => module.resource && module.resource.startsWith(nodeModules) - }) - ].concat(extraPlugins) + plugins: extraPlugins, + optimization: { + // runtimeChunk: 'single', + splitChunks: { + chunks: buildOptions.commonChunk ? 'all' : 'initial', + cacheGroups: { + vendors: false, + vendor: { + name: 'vendor', + chunks: 'initial', + test: (module: any, chunks: Array<{ name: string }>) => { + const moduleName = module.nameForCondition ? module.nameForCondition() : ''; + return /[\\/]node_modules[\\/]/.test(moduleName) + && !chunks.some(({ name }) => name === 'polyfills'); + }, + }, + } + } + }, }; } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts new file mode 100644 index 0000000000..22ca2f0185 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/typescript.ts @@ -0,0 +1,115 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. +import { tags, virtualFs } from '@angular-devkit/core'; +import { Stats } from 'fs'; +import * as path from 'path'; +import { + AngularCompilerPlugin, + AngularCompilerPluginOptions, + PLATFORM +} from '@ngtools/webpack'; +import { buildOptimizerLoader } from './common'; +import { WebpackConfigOptions } from '../build-options'; + +const SilentError = require('silent-error'); + + +const g: any = typeof global !== 'undefined' ? global : {}; +const webpackLoader: string = g['_DevKitIsLocal'] + ? require.resolve('@ngtools/webpack') + : '@ngtools/webpack'; + + +function _createAotPlugin( + wco: WebpackConfigOptions, + options: any, + host: virtualFs.Host, + useMain = true, + extract = false +) { + const { root, buildOptions } = wco; + options.compilerOptions = options.compilerOptions || {}; + + if (wco.buildOptions.preserveSymlinks) { + options.compilerOptions.preserveSymlinks = true; + } + + let i18nInFile = buildOptions.i18nFile + ? path.resolve(root, buildOptions.i18nFile) + : undefined; + + const i18nFileAndFormat = extract + ? { + i18nOutFile: buildOptions.i18nFile, + i18nOutFormat: buildOptions.i18nFormat, + } : { + i18nInFile: i18nInFile, + i18nInFormat: buildOptions.i18nFormat, + }; + + const additionalLazyModules: { [module: string]: string } = {}; + if (buildOptions.lazyModules) { + for (const lazyModule of buildOptions.lazyModules) { + additionalLazyModules[lazyModule] = path.resolve( + root, + lazyModule, + ); + } + } + + const pluginOptions: AngularCompilerPluginOptions = { + mainPath: useMain ? path.join(root, buildOptions.main) : undefined, + ...i18nFileAndFormat, + locale: buildOptions.i18nLocale, + platform: buildOptions.platform === 'server' ? PLATFORM.Server : PLATFORM.Browser, + missingTranslation: buildOptions.i18nMissingTranslation, + sourceMap: buildOptions.sourceMap, + additionalLazyModules, + nameLazyFiles: buildOptions.namedChunks, + forkTypeChecker: buildOptions.forkTypeChecker, + ...options, + host, + }; + return new AngularCompilerPlugin(pluginOptions); +} + +export function getNonAotConfig(wco: WebpackConfigOptions, host: virtualFs.Host) { + const { tsConfigPath } = wco; + + return { + module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, + plugins: [_createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }, host)] + }; +} + +export function getAotConfig( + wco: WebpackConfigOptions, + host: virtualFs.Host, + extract = false +) { + const { tsConfigPath, buildOptions } = wco; + + const loaders: any[] = [webpackLoader]; + if (buildOptions.buildOptimizer) { + loaders.unshift({ + loader: buildOptimizerLoader, + options: { sourceMap: buildOptions.sourceMap } + }); + } + + const test = /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/; + + return { + module: { rules: [{ test, use: loaders }] }, + plugins: [_createAotPlugin(wco, { tsConfigPath }, host, true, extract)] + }; +} + +export function getNonAotTestConfig(wco: WebpackConfigOptions, host: virtualFs.Host) { + const { tsConfigPath } = wco; + + return { + module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, + plugins: [_createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }, host, false)] + }; +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/utils.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts similarity index 53% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/utils.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts index 1fa119ad93..ab1f4f34e9 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/utils.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/utils.ts @@ -2,6 +2,8 @@ // TODO: cleanup this file, it's copied as is from Angular CLI. import * as path from 'path'; +import { basename, normalize } from '@angular-devkit/core'; +import { ExtraEntryPoint, ExtraEntryPointObject } from '../../../browser/schema'; export const ngAppResolve = (resolvePath: string): string => { return path.resolve(process.cwd(), resolvePath); @@ -9,16 +11,19 @@ export const ngAppResolve = (resolvePath: string): string => { const webpackOutputOptions = { colors: true, - hash: true, - timings: true, - chunks: true, + hash: true, // required by custom stat output + timings: true, // required by custom stat output + chunks: true, // required by custom stat output chunkModules: false, children: false, // listing all children is very noisy in AOT and hides warnings/errors modules: false, reasons: false, warnings: true, - assets: false, // listing all assets is very noisy when using assets directories - version: false + errors: true, + assets: true, // required by custom stat output + version: false, + errorDetails: false, + moduleTrace: false, }; const verboseWebpackOutputOptions = { @@ -26,7 +31,9 @@ const verboseWebpackOutputOptions = { assets: true, version: true, reasons: true, - chunkModules: false // TODO: set to true when console to file output is fixed + chunkModules: false, // TODO: set to true when console to file output is fixed + errorDetails: true, + moduleTrace: true, }; export function getWebpackStatsConfig(verbose = false) { @@ -35,43 +42,6 @@ export function getWebpackStatsConfig(verbose = false) { : webpackOutputOptions; } -export interface ExtraEntry { - input: string; - output?: string; - lazy?: boolean; - path?: string; - entry?: string; -} - -// Filter extra entries out of a arran of extraEntries -export function lazyChunksFilter(extraEntries: ExtraEntry[]) { - return extraEntries - .filter(extraEntry => extraEntry.lazy) - .map(extraEntry => extraEntry.entry); -} - -// convert all extra entries into the object representation, fill in defaults -export function extraEntryParser( - extraEntries: (string | ExtraEntry)[], - appRoot: string, - defaultEntry: string -): ExtraEntry[] { - return extraEntries - .map((extraEntry: string | ExtraEntry) => - typeof extraEntry === 'string' ? { input: extraEntry } : extraEntry) - .map((extraEntry: ExtraEntry) => { - extraEntry.path = path.resolve(appRoot, extraEntry.input); - if (extraEntry.output) { - extraEntry.entry = extraEntry.output.replace(/\.(js|css)$/i, ''); - } else if (extraEntry.lazy) { - extraEntry.entry = extraEntry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, ''); - } else { - extraEntry.entry = defaultEntry; - } - return extraEntry; - }); -} - export interface HashFormat { chunk: string; extract: string; @@ -91,9 +61,34 @@ export function getOutputHashFormat(option: string, length = 20): HashFormat { return hashFormats[option] || hashFormats['none']; } -export interface AssetPattern { - glob: string; - input?: string; - output?: string; - allowOutsideOutDir?: boolean; +export type NormalizedEntryPoint = ExtraEntryPointObject & { bundleName: string }; + +export function normalizeExtraEntryPoints( + extraEntryPoints: ExtraEntryPoint[], + defaultBundleName: string +): NormalizedEntryPoint[] { + return extraEntryPoints.map(entry => { + let normalizedEntry; + + if (typeof entry === 'string') { + normalizedEntry = { input: entry, lazy: false, bundleName: defaultBundleName }; + } else { + let bundleName; + + if (entry.bundleName) { + bundleName = entry.bundleName; + } else if (entry.lazy) { + // Lazy entry points use the file name as bundle name. + bundleName = basename( + normalize(entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, '')), + ); + } else { + bundleName = defaultBundleName; + } + + normalizedEntry = {...entry, bundleName}; + } + + return normalizedEntry; + }) } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts new file mode 100644 index 0000000000..6400ce9491 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts @@ -0,0 +1,121 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +/** + * @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 { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; +import { Budget } from '../../browser/schema'; +import { formatSize } from '../utilities/stats'; + +interface Thresholds { + maximumWarning?: number; + maximumError?: number; + minimumWarning?: number; + minimumError?: number; + warningLow?: number; + warningHigh?: number; + errorLow?: number; + errorHigh?: number; +} + +export interface BundleBudgetPluginOptions { + budgets: Budget[]; +} + +export class BundleBudgetPlugin { + constructor(private options: BundleBudgetPluginOptions) { } + + apply(compiler: any): void { + const { budgets } = this.options; + compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: any) => { + if (!budgets || budgets.length === 0) { + return; + } + + budgets.map(budget => { + const thresholds = this.calculate(budget); + return { + budget, + thresholds, + sizes: calculateSizes(budget, compilation) + }; + }) + .forEach(budgetCheck => { + budgetCheck.sizes.forEach(size => { + this.checkMaximum(budgetCheck.thresholds.maximumWarning, size, compilation.warnings); + this.checkMaximum(budgetCheck.thresholds.maximumError, size, compilation.errors); + this.checkMinimum(budgetCheck.thresholds.minimumWarning, size, compilation.warnings); + this.checkMinimum(budgetCheck.thresholds.minimumError, size, compilation.errors); + this.checkMinimum(budgetCheck.thresholds.warningLow, size, compilation.warnings); + this.checkMaximum(budgetCheck.thresholds.warningHigh, size, compilation.warnings); + this.checkMinimum(budgetCheck.thresholds.errorLow, size, compilation.errors); + this.checkMaximum(budgetCheck.thresholds.errorHigh, size, compilation.errors); + }); + + }); + }); + } + + private checkMinimum(threshold: number | undefined, size: Size, messages: any) { + if (threshold) { + if (threshold > size.size) { + const sizeDifference = formatSize(threshold - size.size); + messages.push(`budgets, minimum exceeded for ${size.label}. ` + + `Budget ${formatSize(threshold)} was not reached by ${sizeDifference}.`); + } + } + } + + private checkMaximum(threshold: number | undefined, size: Size, messages: any) { + if (threshold) { + if (threshold < size.size) { + const sizeDifference = formatSize(size.size - threshold); + messages.push(`budgets, maximum exceeded for ${size.label}. ` + + `Budget ${formatSize(threshold)} was exceeded by ${sizeDifference}.`); + } + } + } + + private calculate(budget: Budget): Thresholds { + let thresholds: Thresholds = {}; + if (budget.maximumWarning) { + thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 'pos'); + } + + if (budget.maximumError) { + thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 'pos'); + } + + if (budget.minimumWarning) { + thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, 'neg'); + } + + if (budget.minimumError) { + thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, 'neg'); + } + + if (budget.warning) { + thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, 'neg'); + } + + if (budget.warning) { + thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 'pos'); + } + + if (budget.error) { + thresholds.errorLow = calculateBytes(budget.error, budget.baseline, 'neg'); + } + + if (budget.error) { + thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 'pos'); + } + + return thresholds; + } +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts new file mode 100644 index 0000000000..ca1796810d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts @@ -0,0 +1,142 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +/** + * @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 { Compiler } from 'webpack'; +import { RawSource, SourceMapSource } from 'webpack-sources'; + +const CleanCSS = require('clean-css'); + +interface Chunk { + files: string[]; +} + +export interface CleanCssWebpackPluginOptions { + sourceMap: boolean; + test: (file: string) => boolean; +} + +function hook( + compiler: any, + action: (compilation: any, chunks: Array) => Promise, +) { + if (compiler.hooks) { + // Webpack 4 + compiler.hooks.compilation.tap('cleancss-webpack-plugin', (compilation: any) => { + compilation.hooks.optimizeChunkAssets.tapPromise( + 'cleancss-webpack-plugin', + (chunks: Array) => action(compilation, chunks), + ); + }); + } else { + // Webpack 3 + compiler.plugin('compilation', (compilation: any) => { + compilation.plugin( + 'optimize-chunk-assets', + (chunks: Array, callback: (err?: Error) => void) => action(compilation, chunks) + .then(() => callback()) + .catch((err) => callback(err)), + ); + }); + } +} + +export class CleanCssWebpackPlugin { + private readonly _options: CleanCssWebpackPluginOptions; + + constructor(options: Partial) { + this._options = { + sourceMap: false, + test: (file) => file.endsWith('.css'), + ...options, + }; + } + + apply(compiler: Compiler): void { + hook(compiler, (compilation: any, chunks: Array) => { + const cleancss = new CleanCSS({ + compatibility: 'ie9', + level: 2, + inline: false, + returnPromise: true, + sourceMap: this._options.sourceMap, + }); + + const files: string[] = [...compilation.additionalChunkAssets]; + + chunks.forEach(chunk => { + if (chunk.files && chunk.files.length > 0) { + files.push(...chunk.files); + } + }); + + const actions = files + .filter(file => this._options.test(file)) + .map(file => { + const asset = compilation.assets[file]; + if (!asset) { + return Promise.resolve(); + } + + let content: string; + let map: any; + if (this._options.sourceMap && asset.sourceAndMap) { + const sourceAndMap = asset.sourceAndMap(); + content = sourceAndMap.source; + map = sourceAndMap.map; + } else { + content = asset.source(); + } + + if (content.length === 0) { + return Promise.resolve(); + } + + return Promise.resolve() + .then(() => cleancss.minify(content, map)) + .then((output: any) => { + let hasWarnings = false; + if (output.warnings && output.warnings.length > 0) { + compilation.warnings.push(...output.warnings); + hasWarnings = true; + } + + if (output.errors && output.errors.length > 0) { + output.errors + .forEach((error: string) => compilation.errors.push(new Error(error))); + return; + } + + // generally means invalid syntax so bail + if (hasWarnings && output.stats.minifiedSize === 0) { + return; + } + + let newSource; + if (output.sourceMap) { + newSource = new SourceMapSource( + output.styles, + file, + output.sourceMap.toString(), + content, + map, + ); + } else { + newSource = new RawSource(output.styles); + } + + compilation.assets[file] = newSource; + }); + }); + + return Promise.all(actions); + }); + } +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/index-html-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/index-html-webpack-plugin.ts new file mode 100644 index 0000000000..ede8470dcf --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/index-html-webpack-plugin.ts @@ -0,0 +1,181 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +/** + * @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 { RawSource } from 'webpack-sources'; + +const parse5 = require('parse5'); + +export interface IndexHtmlWebpackPluginOptions { + input: string; + output: string; + baseHref?: string; + entrypoints: string[]; + deployUrl?: string; +} + +function readFile(filename: string, compilation: any): Promise { + return new Promise((resolve, reject) => { + compilation.inputFileSystem.readFile(filename, (err: Error, data: Buffer) => { + if (err) { + reject(err); + return; + } + + let content; + if (data.length >= 3 && data[0] === 0xEF && data[1] === 0xBB && data[2] === 0xBF) { + // Strip UTF-8 BOM + content = data.toString('utf8', 3); + } else if (data.length >= 2 && data[0] === 0xFF && data[1] === 0xFE) { + // Strip UTF-16 LE BOM + content = data.toString('utf16le', 2); + } else { + content = data.toString(); + } + + resolve(content); + }); + }); +} + +export class IndexHtmlWebpackPlugin { + private _options: IndexHtmlWebpackPluginOptions; + + constructor(options?: Partial) { + this._options = { + input: 'index.html', + output: 'index.html', + entrypoints: ['polyfills', 'main'], + ...options + }; + } + + apply(compiler: any) { + compiler.hooks.emit.tapPromise('index-html-webpack-plugin', async (compilation: any) => { + // Get input html file + const inputContent = await readFile(this._options.input, compilation); + compilation.fileDependencies.add(this._options.input); + + + // Get all files for selected entrypoints + const unfilteredSortedFiles: string[] = []; + for (const entryName of this._options.entrypoints) { + const entrypoint = compilation.entrypoints.get(entryName); + if (entrypoint) { + unfilteredSortedFiles.push(...entrypoint.getFiles()); + } + } + + // Filter files + const existingFiles = new Set(); + const stylesheets: string[] = []; + const scripts: string[] = []; + for (const file of unfilteredSortedFiles) { + if (existingFiles.has(file)) { + continue; + } + existingFiles.add(file); + + if (file.endsWith('.js')) { + scripts.push(file); + } else if (file.endsWith('.css')) { + stylesheets.push(file); + } + + } + + // Find the head and body elements + const treeAdapter = parse5.treeAdapters.default; + const document = parse5.parse(inputContent, { treeAdapter }); + let headElement; + let bodyElement; + for (const topNode of document.childNodes) { + if (topNode.tagName === 'html') { + for (const htmlNode of topNode.childNodes) { + if (htmlNode.tagName === 'head') { + headElement = htmlNode; + } + if (htmlNode.tagName === 'body') { + bodyElement = htmlNode; + } + } + } + } + + // Inject into the html + + if (!headElement || !bodyElement) { + throw new Error('Missing head and/or body elements'); + } + + for (const script of scripts) { + const element = treeAdapter.createElement( + 'script', + undefined, + [ + { name: 'type', value: 'text/javascript' }, + { name: 'src', value: (this._options.deployUrl || '') + script }, + ] + ); + treeAdapter.appendChild(bodyElement, element); + } + + // Adjust base href if specified + if (this._options.baseHref != undefined) { + let baseElement; + for (const node of headElement.childNodes) { + if (node.tagName === 'base') { + baseElement = node; + break; + } + } + + if (!baseElement) { + const element = treeAdapter.createElement( + 'base', + undefined, + [ + { name: 'href', value: this._options.baseHref }, + ] + ); + treeAdapter.appendChild(headElement, element); + } else { + let hrefAttribute; + for (const attribute of baseElement.attrs) { + if (attribute.name === 'href') { + hrefAttribute = attribute; + } + } + if (hrefAttribute) { + hrefAttribute.value = this._options.baseHref; + } else { + baseElement.attrs.push({ name: 'href', value: this._options.baseHref }); + } + } + } + + for (const stylesheet of stylesheets) { + const element = treeAdapter.createElement( + 'link', + undefined, + [ + { name: 'rel', value: 'stylesheet' }, + { name: 'href', value: (this._options.deployUrl || '') + stylesheet }, + ] + ); + treeAdapter.appendChild(headElement, element); + } + + // Add to compilation assets + const outputContent = parse5.serialize(document, { treeAdapter }); + compilation.assets[this._options.output] = new RawSource(outputContent); + }); + } +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-context.html b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-context.html similarity index 71% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-context.html rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-context.html index 1c8f49c653..ec185d8d3e 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-context.html +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-context.html @@ -26,13 +26,13 @@ // All served files with the latest timestamps %MAPPINGS% - - + + %SCRIPTS% - - - + + + diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-debug.html b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-debug.html similarity index 73% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-debug.html rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-debug.html index 649d59817e..4f49e825a3 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-debug.html +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-debug.html @@ -28,13 +28,13 @@ // All served files with the latest timestamps %MAPPINGS% - - + + %SCRIPTS% - - - + + + diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-webpack-throw-error.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-webpack-failure-cb.ts similarity index 65% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-webpack-throw-error.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-webpack-failure-cb.ts index 2844ab6025..61050f7e90 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma-webpack-throw-error.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma-webpack-failure-cb.ts @@ -4,13 +4,13 @@ // Force Webpack to throw compilation errors. Useful with karma-webpack when in single-run mode. // Workaround for https://github.com/webpack-contrib/karma-webpack/issues/66 -export class KarmaWebpackThrowError { - constructor() { } +export class KarmaWebpackFailureCb { + constructor(private callback: () => void) { } apply(compiler: any): void { - compiler.plugin('done', (stats: any) => { + compiler.hooks.done.tap('KarmaWebpackFailureCb', (stats: any) => { if (stats.compilation.errors.length > 0) { - throw new Error(stats.compilation.errors.map((err: any) => err.message || err)); + this.callback(); } }); } diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts similarity index 60% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts index e5459978e4..8c33c36fa1 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/karma.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts @@ -7,9 +7,8 @@ import * as glob from 'glob'; import * as webpack from 'webpack'; const webpackDevMiddleware = require('webpack-dev-middleware'); -import { AssetPattern } from '../models/webpack-configs/utils'; -import { isDirectory } from '../utilities/is-directory'; -import { KarmaWebpackThrowError } from './karma-webpack-throw-error'; +import { AssetPattern } from '../../browser/schema'; +import { KarmaWebpackFailureCb } from './karma-webpack-failure-cb'; /** * Enumerate needed (but not require/imported) dependencies from this file @@ -22,6 +21,9 @@ import { KarmaWebpackThrowError } from './karma-webpack-throw-error'; let blocked: any[] = []; let isBlocked = false; +let webpackMiddleware: any; +let successCb: () => void; +let failureCb: () => void; // Add files to the Karma files array. function addKarmaFiles(files: any[], newFiles: any[], prepend = false) { @@ -47,12 +49,15 @@ function addKarmaFiles(files: any[], newFiles: any[], prepend = false) { } const init: any = (config: any, emitter: any, customFileHandlers: any) => { - const options: any = config.webpackBuildFacade.options; - const appRoot = path.join(config.basePath, options.root); + const options = config.buildWebpack.options; + const projectRoot = config.buildWebpack.projectRoot as string; + successCb = config.buildWebpack.successCb; + failureCb = config.buildWebpack.failureCb; - if (options.sourcemaps) { - // Add a reporter that fixes sourcemap urls. - config.reporters.unshift('@angular-devkit/build-webpack'); + config.reporters.unshift('@angular-devkit/build-angular--event-reporter'); + // Add a reporter that fixes sourcemap urls. + if (options.sourceMap) { + config.reporters.unshift('@angular-devkit/build-angular--sourcemap-reporter'); // Code taken from https://github.com/tschaub/karma-source-map-support. // We can't use it directly because we need to add it conditionally in this file, and karma @@ -66,62 +71,17 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { ], true); } - // Add assets. This logic is mimics the one present in GlobCopyWebpackPlugin. - if (options.assets) { - config.proxies = config.proxies || {}; - options.assets.forEach((pattern: AssetPattern) => { - // Convert all string patterns to Pattern type. - pattern = typeof pattern === 'string' ? { glob: pattern } : pattern; - // Add defaults. - // Input is always resolved relative to the appRoot. - pattern.input = path.resolve(appRoot, pattern.input || ''); - pattern.output = pattern.output || ''; - pattern.glob = pattern.glob || ''; - - // Build karma file pattern. - const assetPath = path.join(pattern.input, pattern.glob); - const filePattern = isDirectory(assetPath) ? assetPath + '/**' : assetPath; - addKarmaFiles(config.files, [{ pattern: filePattern, included: false }]); - - // The `files` entry serves the file from `/base/{asset.input}/{asset.glob}`. - // We need to add a URL rewrite that exposes the asset as `/{asset.output}/{asset.glob}`. - let relativePath: string, proxyPath: string; - if (fs.existsSync(assetPath)) { - relativePath = path.relative(config.basePath, assetPath); - proxyPath = path.join(pattern.output, pattern.glob); - } else { - // For globs (paths that don't exist), proxy pattern.output to pattern.input. - relativePath = path.relative(config.basePath, pattern.input); - proxyPath = path.join(pattern.output); - - } - // Proxy paths must have only forward slashes. - proxyPath = proxyPath.replace(/\\/g, '/'); - config.proxies['/' + proxyPath] = '/base/' + relativePath; - }); - } - // Add webpack config. - const webpackConfig = config.webpackBuildFacade.webpackConfig; + const webpackConfig = config.buildWebpack.webpackConfig; const webpackMiddlewareConfig = { - noInfo: true, // Hide webpack output because its noisy. + logLevel: 'error', // Hide webpack output because its noisy. watchOptions: { poll: options.poll }, publicPath: '/_karma_webpack_/', - stats: { // Also prevent chunk and module display output, cleaner look. Only emit errors. - assets: false, - colors: true, - version: false, - hash: false, - timings: false, - chunks: false, - chunkModules: false - } }; - // If Karma is being ran in single run mode, throw errors. - if (config.singleRun) { - webpackConfig.plugins.push(new KarmaWebpackThrowError()); - } + // Finish Karma run early in case of compilation error. + const compilationErrorCb = () => emitter.emit('run_complete', [], { exitCode: 1 }); + webpackConfig.plugins.push(new KarmaWebpackFailureCb(compilationErrorCb)); // Use existing config if any. config.webpack = Object.assign(webpackConfig, config.webpack); @@ -138,9 +98,11 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { config.customContextFile = `${__dirname}/karma-context.html`; config.customDebugFile = `${__dirname}/karma-debug.html`; - // Add the request blocker. + // Add the request blocker and the webpack server fallback. config.beforeMiddleware = config.beforeMiddleware || []; - config.beforeMiddleware.push('devkitBuildWebpackBlocker'); + config.beforeMiddleware.push('@angular-devkit/build-angular--blocker'); + config.middleware = config.middleware || []; + config.middleware.push('@angular-devkit/build-angular--fallback'); // Delete global styles entry, we don't want to load them. delete webpackConfig.entry.styles; @@ -193,20 +155,20 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { } }); - const middleware = new webpackDevMiddleware(compiler, webpackMiddlewareConfig); + webpackMiddleware = new webpackDevMiddleware(compiler, webpackMiddlewareConfig); // Forward requests to webpack server. customFileHandlers.push({ urlRegex: /^\/_karma_webpack_\/.*/, handler: function handler(req: any, res: any) { - middleware(req, res, function () { + webpackMiddleware(req, res, function () { // Ensure script and style bundles are served. // They are mentioned in the custom karma context page and we don't want them to 404. const alwaysServe = [ - '/_karma_webpack_/inline.bundle.js', - '/_karma_webpack_/polyfills.bundle.js', - '/_karma_webpack_/scripts.bundle.js', - '/_karma_webpack_/vendor.bundle.js', + '/_karma_webpack_/runtime.js', + '/_karma_webpack_/polyfills.js', + '/_karma_webpack_/scripts.js', + '/_karma_webpack_/vendor.js', ]; if (alwaysServe.indexOf(req.url) != -1) { res.statusCode = 200; @@ -220,17 +182,13 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { }); emitter.on('exit', (done: any) => { - middleware.close(); + webpackMiddleware.close(); done(); }); }; init.$inject = ['config', 'emitter', 'customFileHandlers']; -// Dummy preprocessor, just to keep karma from showing a warning. -const preprocessor: any = () => (content: any, _file: string, done: any) => done(null, content); -preprocessor.$inject = []; - // Block requests until the Webpack compilation is done. function requestBlocker() { return function (_request: any, _response: any, next: () => void) { @@ -242,9 +200,39 @@ function requestBlocker() { }; } +// Emits builder events. +const eventReporter: any = function (this: any, baseReporterDecorator: any) { + baseReporterDecorator(this); + + this.onRunComplete = function (_browsers: any, results: any) { + if (results.exitCode === 0) { + successCb && successCb(); + } else { + failureCb && failureCb(); + } + } +}; + +eventReporter.$inject = ['baseReporterDecorator']; + // Strip the server address and webpack scheme (webpack://) from error log. -const initSourcemapReporter: any = function (this: any, baseReporterDecorator: any) { +const sourceMapReporter: any = function (this: any, baseReporterDecorator: any, config: any) { baseReporterDecorator(this); + + const reporterName = '@angular/cli'; + const hasTrailingReporters = config.reporters.slice(-1).pop() !== reporterName; + + // Copied from "karma-jasmine-diff-reporter" source code: + // In case, when multiple reporters are used in conjunction + // with initSourcemapReporter, they both will show repetitive log + // messages when displaying everything that supposed to write to terminal. + // So just suppress any logs from initSourcemapReporter by doing nothing on + // browser log, because it is an utility reporter, + // unless it's alone in the "reporters" option and base reporter is used. + if (hasTrailingReporters) { + this.writeCommonMsg = function () { }; + } + const urlRegexp = /\(http:\/\/localhost:\d+\/_karma_webpack_\/webpack:\//gi; this.onSpecComplete = function (_browser: any, result: any) { @@ -256,11 +244,25 @@ const initSourcemapReporter: any = function (this: any, baseReporterDecorator: a }; }; -initSourcemapReporter.$inject = ['baseReporterDecorator']; +sourceMapReporter.$inject = ['baseReporterDecorator', 'config']; -module.exports = Object.assign({ - 'framework:@angular-devkit/build-webpack': ['factory', init], - 'preprocessor:@angular-devkit/build-webpack': ['factory', preprocessor], - 'reporter:@angular-devkit/build-webpack': ['type', initSourcemapReporter], - 'middleware:devkitBuildWebpackBlocker': ['factory', requestBlocker] -}); +// When a request is not found in the karma server, try looking for it from the webpack server root. +function fallbackMiddleware() { + return function (req: any, res: any, next: () => void) { + if (webpackMiddleware) { + const webpackUrl = '/_karma_webpack_' + req.url; + const webpackReq = { ...req, url: webpackUrl } + webpackMiddleware(webpackReq, res, next); + } else { + next(); + } + }; +} + +module.exports = { + 'framework:@angular-devkit/build-angular': ['factory', init], + 'reporter:@angular-devkit/build-angular--sourcemap-reporter': ['type', sourceMapReporter], + 'reporter:@angular-devkit/build-angular--event-reporter': ['type', eventReporter], + 'middleware:@angular-devkit/build-angular--blocker': ['factory', requestBlocker], + 'middleware:@angular-devkit/build-angular--fallback': ['factory', fallbackMiddleware] +}; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts new file mode 100644 index 0000000000..7bf139cd66 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts @@ -0,0 +1,168 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + + +/** + * @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 { interpolateName } from 'loader-utils'; +import * as postcss from 'postcss'; +import * as url from 'url'; +import * as webpack from 'webpack'; + +function wrapUrl(url: string): string { + let wrappedUrl; + const hasSingleQuotes = url.indexOf('\'') >= 0; + + if (hasSingleQuotes) { + wrappedUrl = `"${url}"`; + } else { + wrappedUrl = `'${url}'`; + } + + return `url(${wrappedUrl})`; +} + +export interface PostcssCliResourcesOptions { + deployUrl?: string; + filename: string; + loader: webpack.loader.LoaderContext; +} + +async function resolve( + file: string, + base: string, + resolver: (file: string, base: string) => Promise +): Promise { + try { + return await resolver('./' + file, base); + } catch (err) { + return resolver(file, base); + } +} + +export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResourcesOptions) => { + const { deployUrl, filename, loader } = options; + + const process = async (inputUrl: string, resourceCache: Map) => { + // If root-relative or absolute, leave as is + if (inputUrl.match(/^(?:\w+:\/\/|data:|chrome:|#|\/)/)) { + return inputUrl; + } + // If starts with a caret, remove and return remainder + // this supports bypassing asset processing + if (inputUrl.startsWith('^')) { + return inputUrl.substr(1); + } + + const cachedUrl = resourceCache.get(inputUrl); + if (cachedUrl) { + return cachedUrl; + } + + const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/')); + const resolver = (file: string, base: string) => new Promise((resolve, reject) => { + loader.resolve(base, file, (err, result) => { + if (err) { + reject(err); + return; + } + resolve(result); + }); + }); + + const result = await resolve(pathname as string, loader.context, resolver); + + return new Promise((resolve, reject) => { + loader.fs.readFile(result, (err: Error, content: Buffer) => { + if (err) { + reject(err); + return; + } + + const outputPath = interpolateName( + { resourcePath: result } as webpack.loader.LoaderContext, + filename, + { content }, + ); + + loader.addDependency(result); + loader.emitFile(outputPath, content, undefined); + + let outputUrl = outputPath.replace(/\\/g, '/'); + if (hash || search) { + outputUrl = url.format({ pathname: outputUrl, hash, search }); + } + + if (deployUrl) { + outputUrl = url.resolve(deployUrl, outputUrl); + } + + resourceCache.set(inputUrl, outputUrl); + + resolve(outputUrl); + }); + }); + }; + + return (root) => { + const urlDeclarations: Array = []; + root.walkDecls(decl => { + if (decl.value && decl.value.includes('url')) { + urlDeclarations.push(decl); + } + }); + + if (urlDeclarations.length === 0) { + return; + } + + const resourceCache = new Map(); + + return Promise.all(urlDeclarations.map(async decl => { + const value = decl.value; + const urlRegex = /url\(\s*(?:"([^"]+)"|'([^']+)'|(.+?))\s*\)/g; + const segments: string[] = []; + + let match; + let lastIndex = 0; + let modified = false; + // tslint:disable-next-line:no-conditional-assignment + while (match = urlRegex.exec(value)) { + const originalUrl = match[1] || match[2] || match[3]; + let processedUrl; + try { + processedUrl = await process(originalUrl, resourceCache); + } catch (err) { + loader.emitError(decl.error(err.message, { word: originalUrl }).toString()); + continue; + } + + if (lastIndex < match.index) { + segments.push(value.slice(lastIndex, match.index)); + } + + if (!processedUrl || originalUrl === processedUrl) { + segments.push(match[0]); + } else { + segments.push(wrapUrl(processedUrl)); + modified = true; + } + + lastIndex = match.index + match[0].length; + } + + if (lastIndex < value.length) { + segments.push(value.slice(lastIndex)); + } + + if (modified) { + decl.value = segments.join(''); + } + })); + }; +}); diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/raw-css-loader.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/raw-css-loader.ts new file mode 100644 index 0000000000..6b150b0b7f --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/raw-css-loader.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export default function(content: string, map: object) { + const stringifiedContent = JSON.stringify(content); + const stringifiedMap = map && JSON.stringify(map); + + return `module.exports = [[module.id, ${stringifiedContent}, '', ${stringifiedMap}]]`; +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/scripts-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/scripts-webpack-plugin.ts new file mode 100644 index 0000000000..d832a124ad --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/scripts-webpack-plugin.ts @@ -0,0 +1,190 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +/** + * @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 + */ +/** + * @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 { Compiler, loader } from 'webpack'; +import { CachedSource, ConcatSource, OriginalSource, RawSource, Source } from 'webpack-sources'; +import { interpolateName } from 'loader-utils'; +import * as path from 'path'; + +const Chunk = require('webpack/lib/Chunk'); +const EntryPoint = require('webpack/lib/Entrypoint'); + +export interface ScriptsWebpackPluginOptions { + name: string; + sourceMap: boolean; + scripts: string[]; + filename: string; + basePath: string; +} + +interface ScriptOutput { + filename: string; + source: CachedSource; +} + +function addDependencies(compilation: any, scripts: string[]): void { + if (compilation.fileDependencies.add) { + // Webpack 4+ uses a Set + for (const script of scripts) { + compilation.fileDependencies.add(script); + } + } else { + // Webpack 3 + compilation.fileDependencies.push(...scripts); + } +} + +function hook(compiler: any, action: (compilation: any, callback: (err?: Error) => void) => void) { + if (compiler.hooks) { + // Webpack 4 + compiler.hooks.thisCompilation.tap('scripts-webpack-plugin', (compilation: any) => { + compilation.hooks.additionalAssets.tapAsync( + 'scripts-webpack-plugin', + (callback: (err?: Error) => void) => action(compilation, callback), + ); + }); + } else { + // Webpack 3 + compiler.plugin('this-compilation', (compilation: any) => { + compilation.plugin( + 'additional-assets', + (callback: (err?: Error) => void) => action(compilation, callback), + ); + }); + } +} + +export class ScriptsWebpackPlugin { + private _lastBuildTime?: number; + private _cachedOutput?: ScriptOutput; + + constructor(private options: Partial = {}) { } + + shouldSkip(compilation: any, scripts: string[]): boolean { + if (this._lastBuildTime == undefined) { + this._lastBuildTime = Date.now(); + return false; + } + + for (let i = 0; i < scripts.length; i++) { + let scriptTime; + if (compilation.fileTimestamps.get) { + // Webpack 4+ uses a Map + scriptTime = compilation.fileTimestamps.get(scripts[i]); + } else { + // Webpack 3 + scriptTime = compilation.fileTimestamps[scripts[i]]; + } + if (!scriptTime || scriptTime > this._lastBuildTime) { + this._lastBuildTime = Date.now(); + return false; + } + } + + return true; + } + + private _insertOutput(compilation: any, { filename, source }: ScriptOutput, cached = false) { + const chunk = new Chunk(this.options.name); + chunk.rendered = !cached; + chunk.id = this.options.name; + chunk.ids = [chunk.id]; + chunk.files.push(filename); + + const entrypoint = new EntryPoint(this.options.name); + entrypoint.pushChunk(chunk); + + compilation.entrypoints.set(this.options.name, entrypoint); + compilation.chunks.push(chunk); + compilation.assets[filename] = source; + } + + apply(compiler: Compiler): void { + if (!this.options.scripts || this.options.scripts.length === 0) { + return; + } + + const scripts = this.options.scripts + .filter(script => !!script) + .map(script => path.resolve(this.options.basePath || '', script)); + + hook(compiler, (compilation, callback) => { + if (this.shouldSkip(compilation, scripts)) { + if (this._cachedOutput) { + this._insertOutput(compilation, this._cachedOutput, true); + } + + addDependencies(compilation, scripts); + callback(); + + return; + } + + const sourceGetters = scripts.map(fullPath => { + return new Promise((resolve, reject) => { + compilation.inputFileSystem.readFile(fullPath, (err: Error, data: Buffer) => { + if (err) { + reject(err); + return; + } + + const content = data.toString(); + + let source; + if (this.options.sourceMap) { + // TODO: Look for source map file (for '.min' scripts, etc.) + + let adjustedPath = fullPath; + if (this.options.basePath) { + adjustedPath = path.relative(this.options.basePath, fullPath); + } + source = new OriginalSource(content, adjustedPath); + } else { + source = new RawSource(content); + } + + resolve(source); + }); + }); + }); + + Promise.all(sourceGetters) + .then(sources => { + const concatSource = new ConcatSource(); + sources.forEach(source => { + concatSource.add(source); + concatSource.add('\n;'); + }); + + const combinedSource = new CachedSource(concatSource); + const filename = interpolateName( + { resourcePath: 'scripts.js' } as loader.LoaderContext, + this.options.filename as string, + { content: combinedSource.source() }, + ); + + const output = { filename, source: combinedSource }; + this._insertOutput(compilation, output); + this._cachedOutput = output; + addDependencies(compilation, scripts); + + callback(); + }) + .catch((err: Error) => callback(err)); + }); + } +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts similarity index 66% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts index 8b3c84d695..378443b2a2 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/suppress-entry-chunks-webpack-plugin.ts @@ -8,22 +8,22 @@ export class SuppressExtractedTextChunksWebpackPlugin { constructor() { } apply(compiler: any): void { - compiler.plugin('compilation', function (compilation: any) { + compiler.hooks.compilation.tap('SuppressExtractedTextChunks', (compilation: any) => { // find which chunks have css only entry points const cssOnlyChunks: string[] = []; const entryPoints = compilation.options.entry; // determine which entry points are composed entirely of css files for (let entryPoint of Object.keys(entryPoints)) { - let entryFiles: string[]|string = entryPoints[entryPoint]; + let entryFiles: string[] | string = entryPoints[entryPoint]; // when type of entryFiles is not array, make it as an array entryFiles = entryFiles instanceof Array ? entryFiles : [entryFiles]; if (entryFiles.every((el: string) => el.match(/\.(css|scss|sass|less|styl)$/) !== null)) { - cssOnlyChunks.push(entryPoint); + cssOnlyChunks.push(entryPoint); } } // Remove the js file for supressed chunks - compilation.plugin('after-seal', (callback: any) => { + compilation.hooks.afterSeal.tap('SuppressExtractedTextChunks', () => { compilation.chunks .filter((chunk: any) => cssOnlyChunks.indexOf(chunk.name) !== -1) .forEach((chunk: any) => { @@ -38,18 +38,18 @@ export class SuppressExtractedTextChunksWebpackPlugin { }); chunk.files = newFiles; }); - callback(); }); // Remove scripts tags with a css file as source, because HtmlWebpackPlugin will use // a css file as a script for chunks without js files. - compilation.plugin('html-webpack-plugin-alter-asset-tags', - (htmlPluginData: any, callback: any) => { - const filterFn = (tag: any) => - !(tag.tagName === 'script' && tag.attributes.src.match(/\.css$/)); - htmlPluginData.head = htmlPluginData.head.filter(filterFn); - htmlPluginData.body = htmlPluginData.body.filter(filterFn); - callback(null, htmlPluginData); - }); + // TODO: Enable this once HtmlWebpackPlugin supports Webpack 4 + // compilation.plugin('html-webpack-plugin-alter-asset-tags', + // (htmlPluginData: any, callback: any) => { + // const filterFn = (tag: any) => + // !(tag.tagName === 'script' && tag.attributes.src.match(/\.css$/)); + // htmlPluginData.head = htmlPluginData.head.filter(filterFn); + // htmlPluginData.body = htmlPluginData.body.filter(filterFn); + // callback(null, htmlPluginData); + // }); }); } } diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/webpack.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts similarity index 73% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/webpack.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts index 972e2782b4..a32209e0dd 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/webpack.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts @@ -4,8 +4,13 @@ // Exports the webpack plugins we use internally. export { BaseHrefWebpackPlugin } from '../lib/base-href-webpack/base-href-webpack-plugin'; export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss-webpack-plugin'; -export { GlobCopyWebpackPlugin, GlobCopyWebpackPluginOptions } from './glob-copy-webpack-plugin'; export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget'; -export { NamedLazyChunksWebpackPlugin } from './named-lazy-chunks-webpack-plugin'; export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; +export { + default as PostcssCliResources, + PostcssCliResourcesOptions, +} from './postcss-cli-resources'; + +import { join } from 'path'; +export const RawCssLoader = require.resolve(join(__dirname, 'raw-css-loader')); diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts similarity index 83% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/bundle-calculator.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts index 59d4b54903..0df3d1e333 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts @@ -8,46 +8,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -export type BudgetType = 'all' | 'allScript' | 'any' | 'anyScript' | 'bundle' | 'initial'; - -export interface Budget { - /** - * The type of budget - */ - type: BudgetType; - /** - * The name of the bundle - */ - name?: string; - /** - * The baseline size for comparison. - */ - baseline?: string; - /** - * The maximum threshold for warning relative to the baseline. - */ - maximumWarning?: string; - /** - * The maximum threshold for error relative to the baseline. - */ - maximumError?: string; - /** - * The minimum threshold for warning relative to the baseline. - */ - minimumWarning?: string; - /** - * The minimum threshold for error relative to the baseline. - */ - minimumError?: string; - /** - * The threshold for warning relative to the baseline (min & max). - */ - warning?: string; - /** - * The threshold for error relative to the baseline (min & max). - */ - error?: string; -} +import { Budget } from '../../browser/schema'; export interface Compilation { assets: any; diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/check-port.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/check-port.ts similarity index 95% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/check-port.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/check-port.ts index 6f5478da19..316dac751e 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/check-port.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/check-port.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; const portfinder = require('portfinder'); diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/find-up.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/find-up.ts similarity index 100% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/find-up.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/find-up.ts diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/is-directory.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/is-directory.ts similarity index 100% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/is-directory.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/is-directory.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts new file mode 100644 index 0000000000..35cf0db898 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/package-chunk-sort.ts @@ -0,0 +1,51 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +import { ExtraEntryPoint } from '../../browser/schema'; +import { normalizeExtraEntryPoints } from '../models/webpack-configs/utils'; + +export function generateEntryPoints(appConfig: any) { + let entryPoints = ['polyfills', 'sw-register']; + + // Add all styles/scripts, except lazy-loaded ones. + [ + ...normalizeExtraEntryPoints(appConfig.styles as ExtraEntryPoint[], 'styles') + .filter(entry => !entry.lazy) + .map(entry => entry.bundleName), + ...normalizeExtraEntryPoints(appConfig.scripts as ExtraEntryPoint[], 'scripts') + .filter(entry => !entry.lazy) + .map(entry => entry.bundleName), + ].forEach(bundleName => { + if (entryPoints.indexOf(bundleName) === -1) { + entryPoints.push(bundleName); + } + }); + + entryPoints.push('main'); + + return entryPoints; +} + +// Sort chunks according to a predefined order: +// inline, polyfills, all styles, vendor, main +export function packageChunkSort(appConfig: any) { + const entryPoints = generateEntryPoints(appConfig); + + function sort(left: any, right: any) { + let leftIndex = entryPoints.indexOf(left.names[0]); + let rightindex = entryPoints.indexOf(right.names[0]); + + if (leftIndex > rightindex) { + return 1; + } else if (leftIndex < rightindex) { + return -1; + } else { + return 0; + } + } + + // We need to list of entry points for the Ejected webpack config to work (we reuse the function + // defined above). + (sort as any).entryPoints = entryPoints; + return sort; +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/read-tsconfig.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/read-tsconfig.ts similarity index 89% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/read-tsconfig.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/read-tsconfig.ts index c8638cd1d6..6ff083a719 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/read-tsconfig.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/read-tsconfig.ts @@ -1,11 +1,11 @@ // tslint:disable // TODO: cleanup this file, it's copied as is from Angular CLI. - +import * as ts from 'typescript'; import * as path from 'path'; import { requireProjectModule } from '../utilities/require-project-module'; export function readTsconfig(tsconfigPath: string) { - const projectTs = requireProjectModule(path.dirname(tsconfigPath), 'typescript'); + const projectTs = requireProjectModule(path.dirname(tsconfigPath), 'typescript') as typeof ts; const configResult = projectTs.readConfigFile(tsconfigPath, projectTs.sys.readFile); const tsConfig = projectTs.parseJsonConfigFileContent(configResult.config, projectTs.sys, path.dirname(tsconfigPath), undefined, tsconfigPath); diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/require-project-module.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/require-project-module.ts new file mode 100644 index 0000000000..79b17d1c49 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/require-project-module.ts @@ -0,0 +1,14 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. + +const resolve = require('resolve'); + +// Resolve dependencies within the target project. +export function resolveProjectModule(root: string, moduleName: string) { + return resolve.sync(moduleName, { basedir: root }); +} + +// Require dependencies within the target project. +export function requireProjectModule(root: string, moduleName: string) { + return require(resolveProjectModule(root, moduleName)); +} diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts new file mode 100644 index 0000000000..f0427f0d21 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts @@ -0,0 +1,147 @@ +// tslint:disable +// TODO: cleanup this file, it's copied as is from Angular CLI. +import { Path, join, normalize, virtualFs, dirname, getSystemPath, tags } from '@angular-devkit/core'; +import { Filesystem } from '@angular/service-worker/config'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as semver from 'semver'; + +import { resolveProjectModule } from '../require-project-module'; +import { map, reduce, switchMap } from "rxjs/operators"; +import { Observable, merge, of } from "rxjs"; + + +export const NEW_SW_VERSION = '5.0.0-rc.0'; + + +class CliFilesystem implements Filesystem { + constructor(private _host: virtualFs.Host, private base: string) { } + + list(path: string): Promise { + return this._host.list(this._resolve(path)).toPromise().then(x => x, _err => []); + } + + read(path: string): Promise { + return this._host.read(this._resolve(path)) + .toPromise() + .then(content => virtualFs.fileBufferToString(content)); + } + + hash(path: string): Promise { + const sha1 = crypto.createHash('sha1'); + + return this.read(path) + .then(content => sha1.update(content)) + .then(() => sha1.digest('hex')); + } + + write(path: string, content: string): Promise { + return this._host.write(this._resolve(path), virtualFs.stringToFileBuffer(content)) + .toPromise(); + } + + private _resolve(path: string): Path { + return join(normalize(this.base), path); + } +} + +export function usesServiceWorker(projectRoot: string): boolean { + let swPackageJsonPath; + + try { + swPackageJsonPath = resolveProjectModule(projectRoot, '@angular/service-worker/package.json'); + } catch (_) { + // @angular/service-worker is not installed + throw new Error(tags.stripIndent` + Your project is configured with serviceWorker = true, but @angular/service-worker + is not installed. Run \`npm install --save-dev @angular/service-worker\` + and try again, or run \`ng set apps.0.serviceWorker=false\` in your .angular-cli.json. + `); + } + + const swPackageJson = fs.readFileSync(swPackageJsonPath).toString(); + const swVersion = JSON.parse(swPackageJson)['version']; + + if (!semver.gte(swVersion, NEW_SW_VERSION)) { + throw new Error(tags.stripIndent` + The installed version of @angular/service-worker is ${swVersion}. This version of the CLI + requires the @angular/service-worker version to satisfy ${NEW_SW_VERSION}. Please upgrade + your service worker version. + `); + } + + return true; +} + +export function augmentAppWithServiceWorker( + host: virtualFs.Host, + projectRoot: Path, + appRoot: Path, + outputPath: Path, + baseHref: string, +): Promise { + // Path to the worker script itself. + const distPath = normalize(outputPath); + const workerPath = normalize( + resolveProjectModule(getSystemPath(projectRoot), '@angular/service-worker/ngsw-worker.js'), + ); + const swConfigPath = resolveProjectModule( + getSystemPath(projectRoot), + '@angular/service-worker/config', + ); + const safetyPath = join(dirname(workerPath), 'safety-worker.js'); + const configPath = join(appRoot, 'ngsw-config.json'); + + return host.exists(configPath).pipe( + switchMap(exists => { + if (!exists) { + throw new Error(tags.oneLine` + Error: Expected to find an ngsw-config.json configuration + file in the ${appRoot} folder. Either provide one or disable Service Worker + in your angular.json configuration file.`, + ); + } + + return host.read(configPath) as Observable; + }), + map(content => JSON.parse(virtualFs.fileBufferToString(content))), + switchMap(configJson => { + const Generator = require(swConfigPath).Generator; + const gen = new Generator(new CliFilesystem(host, outputPath), baseHref); + + return gen.process(configJson); + }), + + switchMap(output => { + const manifest = JSON.stringify(output, null, 2); + return host.read(workerPath).pipe( + switchMap(workerCode => { + return merge( + host.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest)), + host.write(join(distPath, 'ngsw-worker.js'), workerCode), + ) as Observable; + }), + ); + }), + + switchMap(() => host.exists(safetyPath)), + // If @angular/service-worker has the safety script, copy it into two locations. + switchMap(exists => { + if (!exists) { + return of(undefined); + } + + return host.read(safetyPath).pipe( + switchMap(safetyCode => { + return merge( + host.write(join(distPath, 'worker-basic.min.js'), safetyCode), + host.write(join(distPath, 'safety-worker.js'), safetyCode), + ) as Observable; + }), + ); + }), + + // Remove all elements, reduce them to a single emit. + reduce(() => {}), + ).toPromise(); +} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/stats.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts similarity index 85% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/stats.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts index 7017906443..6c135c3722 100644 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/stats.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/stats.ts @@ -2,7 +2,7 @@ // TODO: cleanup this file, it's copied as is from Angular CLI. import chalk from 'chalk'; -import { stripIndents } from 'common-tags'; +import { tags } from '@angular-devkit/core'; // Force basic color support on terminals with no color support. @@ -10,7 +10,7 @@ import { stripIndents } from 'common-tags'; const chalkCtx = new (chalk.constructor as any)(chalk.supportsColor ? {} : { level: 1 }); const { bold, green, red, reset, white, yellow } = chalkCtx; -function _formatSize(size: number): string { +export function formatSize(size: number): string { if (size <= 0) { return '0 bytes'; } @@ -32,8 +32,8 @@ export function statsToString(json: any, statsConfig: any) { const changedChunksStats = json.chunks .filter((chunk: any) => chunk.rendered) .map((chunk: any) => { - const asset = (json.assets || []).filter((x: any) => x.name == chunk.files[0])[0]; - const size = asset ? ` ${_formatSize(asset.size)}` : ''; + const asset = json.assets.filter((x: any) => x.name == chunk.files[0])[0]; + const size = asset ? ` ${formatSize(asset.size)}` : ''; const files = chunk.files.join(', '); const names = chunk.names ? ` (${chunk.names.join(', ')})` : ''; const initial = y(chunk.entry ? '[entry]' : chunk.initial ? '[initial]' : ''); @@ -47,13 +47,13 @@ export function statsToString(json: any, statsConfig: any) { const unchangedChunkNumber = json.chunks.length - changedChunksStats.length; if (unchangedChunkNumber > 0) { - return rs(stripIndents` - Date: ${w(new Date().toISOString())} • Hash: ${w(json.hash)} • Time: ${w('' + json.time)}ms + return '\n' + rs(tags.stripIndents` + Date: ${w(new Date().toISOString())} - Hash: ${w(json.hash)} - Time: ${w('' + json.time)}ms ${unchangedChunkNumber} unchanged chunks ${changedChunksStats.join('\n')} `); } else { - return rs(stripIndents` + return '\n' + rs(tags.stripIndents` Date: ${w(new Date().toISOString())} Hash: ${w(json.hash)} Time: ${w('' + json.time)}ms diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/strip-bom.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/strip-bom.ts similarity index 100% rename from packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/strip-bom.ts rename to packages/angular_devkit/build_angular/src/angular-cli-files/utilities/strip-bom.ts diff --git a/packages/angular_devkit/build_angular/src/app-shell/index.ts b/packages/angular_devkit/build_angular/src/app-shell/index.ts new file mode 100644 index 0000000000..4064551d2b --- /dev/null +++ b/packages/angular_devkit/build_angular/src/app-shell/index.ts @@ -0,0 +1,174 @@ +/** + * @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 { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core'; +import { Observable, forkJoin, from, merge, of, throwError } from 'rxjs'; +import { concatMap, map, switchMap } from 'rxjs/operators'; +import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; +import { BuildWebpackServerSchema } from '../server/schema'; +import { BuildWebpackAppShellSchema } from './schema'; + + +export class AppShellBuilder implements Builder { + + constructor(public context: BuilderContext) { } + + run(builderConfig: BuilderConfiguration): Observable { + const options = builderConfig.options; + + return new Observable(obs => { + let success = true; + const subscription = merge( + this.build(options.serverTarget, {}), + this.build(options.browserTarget, { watch: false }), + ).subscribe((event: BuildEvent) => { + // TODO: once we support a better build event, add support for merging two event streams + // together. + success = success && event.success; + }, error => { + obs.error(error); + }, () => { + obs.next({ success }); + obs.complete(); + }); + + // Allow subscriptions to us to unsubscribe from each builds at the same time. + return () => subscription.unsubscribe(); + }).pipe( + switchMap(event => { + if (!event.success) { + return of(event); + } + + return this.renderUniversal(options); + }), + ); + } + + build(targetString: string, overrides: {}) { + const architect = this.context.architect; + const [project, target, configuration] = targetString.split(':'); + + // Override browser build watch setting. + const builderConfig = architect.getBuilderConfiguration<{}>({ + project, + target, + configuration, + overrides, + }); + + return architect.run(builderConfig, this.context); + } + + getServerModuleBundlePath(options: BuildWebpackAppShellSchema) { + const architect = this.context.architect; + + return new Observable(obs => { + if (options.appModuleBundle) { + obs.next(join(this.context.workspace.root, options.appModuleBundle)); + + return obs.complete(); + } else { + const [project, target, configuration] = options.serverTarget.split(':'); + const builderConfig = architect.getBuilderConfiguration({ + project, + target, + configuration, + }); + + return architect.getBuilderDescription(builderConfig).pipe( + concatMap(description => architect.validateBuilderOptions(builderConfig, description)), + switchMap(config => { + const outputPath = join(this.context.workspace.root, config.options.outputPath); + + return this.context.host.list(outputPath).pipe( + switchMap(files => { + const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?(?:bundle\.)?js$/; + const maybeMain = files.filter(x => re.test(x))[0]; + + if (!maybeMain) { + return throwError(new Error('Could not find the main bundle.')); + } else { + return of(join(outputPath, maybeMain)); + } + }), + ); + }), + ).subscribe(obs); + } + }); + } + + getBrowserIndexOutputPath(options: BuildWebpackAppShellSchema) { + const architect = this.context.architect; + const [project, target, configuration] = options.browserTarget.split(':'); + const builderConfig = architect.getBuilderConfiguration({ + project, + target, + configuration, + }); + + return architect.getBuilderDescription(builderConfig).pipe( + concatMap(description => architect.validateBuilderOptions(builderConfig, description)), + map(config => join(normalize(config.options.outputPath), 'index.html')), + ); + } + + renderUniversal(options: BuildWebpackAppShellSchema): Observable { + return forkJoin( + this.getBrowserIndexOutputPath(options).pipe( + switchMap(browserIndexOutputPath => { + const path = join(this.context.workspace.root, browserIndexOutputPath); + + return this.context.host.read(path).pipe( + map(x => { + return [browserIndexOutputPath, x]; + }), + ); + }), + ), + this.getServerModuleBundlePath(options), + ).pipe( + switchMap(([[browserIndexOutputPath, indexContent], serverBundlePath]) => { + const root = this.context.workspace.root; + requireProjectModule(getSystemPath(root), 'zone.js/dist/zone-node'); + + const renderModuleFactory = requireProjectModule( + getSystemPath(root), + '@angular/platform-server', + ).renderModuleFactory; + const AppServerModuleNgFactory = require( + getSystemPath(serverBundlePath), + ).AppServerModuleNgFactory; + const indexHtml = virtualFs.fileBufferToString(indexContent); + const outputPath = join(root, options.outputIndexPath || browserIndexOutputPath); + + // Render to HTML and overwrite the client index file. + return from( + renderModuleFactory(AppServerModuleNgFactory, { + document: indexHtml, + url: options.route, + }) + .then((html: string) => { + return this.context.host + .write(outputPath, virtualFs.stringToFileBuffer(html)) + .toPromise(); + }) + .then(() => ({ success: true })), + ); + }), + ); + } +} + +export default AppShellBuilder; diff --git a/packages/angular_devkit/build_angular/src/app-shell/schema.d.ts b/packages/angular_devkit/build_angular/src/app-shell/schema.d.ts new file mode 100644 index 0000000000..b7f0b1d1de --- /dev/null +++ b/packages/angular_devkit/build_angular/src/app-shell/schema.d.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface BuildWebpackAppShellSchema { + browserTarget: string; + serverTarget: string; + appModuleBundle?: string; + route: string; + inputIndexPath?: string; + outputIndexPath?: string; +} diff --git a/packages/angular_devkit/build_angular/src/app-shell/schema.json b/packages/angular_devkit/build_angular/src/app-shell/schema.json new file mode 100644 index 0000000000..4e246a6f83 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/app-shell/schema.json @@ -0,0 +1,37 @@ +{ + "title": "App Shell Target", + "description": "App Shell target options for Build Facade.", + "type": "object", + "properties": { + "browserTarget": { + "type": "string", + "description": "Target to build." + }, + "serverTarget": { + "type": "string", + "description": "Server target to use for rendering the app shell." + }, + "appModuleBundle": { + "type": "string", + "description": "Script that exports the Server AppModule to render. This should be the main JavaScript outputted by the server target. By default we will resolve the outputPath of the serverTarget and find a bundle named 'main' in it (whether or not there's a hash tag)." + }, + "route": { + "type": "string", + "description": "The route to render.", + "default": "/" + }, + "inputIndexPath": { + "type": "string", + "description": "The input path for the index.html file. By default uses the output index.html of the browser target." + }, + "outputIndexPath": { + "type": "string", + "description": "The output path of the index.html file. By default will overwrite the input file." + } + }, + "additionalProperties": false, + "required": [ + "browserTarget", + "serverTarget" + ] +} diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts new file mode 100644 index 0000000000..9402e23cb9 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -0,0 +1,208 @@ +/** + * @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 { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import * as fs from 'fs'; +import { Observable, concat, of } from 'rxjs'; +import { concatMap, last } from 'rxjs/operators'; +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies +import * as webpack from 'webpack'; +import { + getAotConfig, + getBrowserConfig, + getCommonConfig, + getNonAotConfig, + getStylesConfig, +} from '../angular-cli-files/models/webpack-configs'; +import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils'; +import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; +import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; +import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker'; +import { + statsErrorsToString, + statsToString, + statsWarningsToString, +} from '../angular-cli-files/utilities/stats'; +import { addFileReplacements } from '../utils'; +import { BrowserBuilderSchema } from './schema'; +const webpackMerge = require('webpack-merge'); + + +export interface WebpackConfigOptions { + root: string; + projectRoot: string; + buildOptions: BrowserBuilderSchema; + tsConfig: ts.ParsedCommandLine; + tsConfigPath: string; + supportES2015: boolean; +} + +export class BrowserBuilder implements Builder { + + constructor(public context: BuilderContext) { } + + run(builderConfig: BuilderConfiguration): Observable { + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); + const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); + + return of(null).pipe( + concatMap(() => options.deleteOutputPath + ? this._deleteOutputDir(root, normalize(options.outputPath), this.context.host) + : of(null)), + concatMap(() => addFileReplacements(root, host, options.fileReplacements)), + concatMap(() => new Observable(obs => { + // Ensure Build Optimizer is only used with AOT. + if (options.buildOptimizer && !options.aot) { + throw new Error('The `--build-optimizer` option cannot be used without `--aot`.'); + } + + let webpackConfig; + try { + webpackConfig = this.buildWebpackConfig(root, projectRoot, host, options); + } catch (e) { + obs.error(e); + + return; + } + const webpackCompiler = webpack(webpackConfig); + const statsConfig = getWebpackStatsConfig(options.verbose); + + const callback: webpack.compiler.CompilerCallback = (err, stats) => { + if (err) { + return obs.error(err); + } + + const json = stats.toJson(statsConfig); + if (options.verbose) { + this.context.logger.info(stats.toString(statsConfig)); + } else { + this.context.logger.info(statsToString(json, statsConfig)); + } + + if (stats.hasWarnings()) { + this.context.logger.warn(statsWarningsToString(json, statsConfig)); + } + if (stats.hasErrors()) { + this.context.logger.error(statsErrorsToString(json, statsConfig)); + } + + if (options.watch) { + obs.next({ success: !stats.hasErrors() }); + + // Never complete on watch mode. + return; + } else { + if (builderConfig.options.serviceWorker) { + augmentAppWithServiceWorker( + this.context.host, + root, + projectRoot, + resolve(root, normalize(options.outputPath)), + options.baseHref || '/', + ).then( + () => { + obs.next({ success: !stats.hasErrors() }); + obs.complete(); + }, + (err: Error) => { + // We error out here because we're not in watch mode anyway (see above). + obs.error(err); + }, + ); + } else { + obs.next({ success: !stats.hasErrors() }); + obs.complete(); + } + } + }; + + try { + if (options.watch) { + const watching = webpackCompiler.watch({ poll: options.poll }, callback); + + // Teardown logic. Close the watcher when unsubscribed from. + return () => watching.close(() => { }); + } else { + webpackCompiler.run(callback); + } + } catch (err) { + if (err) { + this.context.logger.error( + '\nAn error occured during the build:\n' + ((err && err.stack) || err)); + } + throw err; + } + })), + ); + } + + buildWebpackConfig( + root: Path, + projectRoot: Path, + host: virtualFs.Host, + options: BrowserBuilderSchema, + ) { + let wco: WebpackConfigOptions; + + const tsConfigPath = getSystemPath(normalize(resolve(root, normalize(options.tsConfig)))); + const tsConfig = readTsconfig(tsConfigPath); + + const projectTs = requireProjectModule(getSystemPath(projectRoot), 'typescript') as typeof ts; + + const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && tsConfig.options.target !== projectTs.ScriptTarget.ES5; + + wco = { + root: getSystemPath(root), + projectRoot: getSystemPath(projectRoot), + buildOptions: options, + tsConfig, + tsConfigPath, + supportES2015, + }; + + const webpackConfigs: {}[] = [ + getCommonConfig(wco), + getBrowserConfig(wco), + getStylesConfig(wco), + ]; + + if (wco.buildOptions.main || wco.buildOptions.polyfills) { + const typescriptConfigPartial = wco.buildOptions.aot + ? getAotConfig(wco, host) + : getNonAotConfig(wco, host); + webpackConfigs.push(typescriptConfigPartial); + } + + return webpackMerge(webpackConfigs); + } + + private _deleteOutputDir(root: Path, outputPath: Path, host: virtualFs.Host) { + const resolvedOutputPath = resolve(root, outputPath); + if (resolvedOutputPath === root) { + throw new Error('Output path MUST not be project root directory!'); + } + + return host.exists(resolvedOutputPath).pipe( + concatMap(exists => exists + // TODO: remove this concat once host ops emit an event. + ? concat(host.delete(resolvedOutputPath), of(null)).pipe(last()) + // ? of(null) + : of(null)), + ); + } +} + +export default BrowserBuilder; diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts new file mode 100644 index 0000000000..7a551cf926 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -0,0 +1,353 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface BrowserBuilderSchema { + /** + * List of static application assets. + */ + assets: AssetPattern[]; + + /** + * The name of the main entry-point file. + */ + main: string; + + /** + * The name of the polyfills file. + */ + polyfills?: string; + + /** + * The name of the TypeScript configuration file. + */ + tsConfig: string; + + /** + * Global scripts to be included in the build. + */ + scripts: ExtraEntryPoint[]; + + /** + * Global styles to be included in the build. + */ + styles: ExtraEntryPoint[]; + + /** + * Options to pass to style preprocessors. + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + + /** + * Defines the optimization level of the build. + */ + optimization: boolean; + + /** + * Replace files with other files in the build. + */ + fileReplacements: FileReplacements[]; + + /** + * Path where output will be placed. + */ + outputPath: string; + + /** + * Build using Ahead of Time compilation. + */ + aot: boolean; + + /** + * Output sourcemaps. + */ + sourceMap: boolean; + + /** + * Output in-file eval sourcemaps. + */ + evalSourceMap: boolean; + + /** + * Use a separate bundle containing only vendor libraries. + */ + vendorChunk: boolean; + + /** + * Use a separate bundle containing code used across multiple bundles. + */ + commonChunk: boolean; + + /** + * Base url for the application being built. + */ + baseHref?: string; + + /** + * URL where files will be deployed. + */ + deployUrl?: string; + + /** + * Adds more details to output logging. + */ + verbose: boolean; + + /** + * Log progress to the console while building. + */ + progress: boolean; + + /** + * Localization file to use for i18n. + */ + i18nFile?: string; + + /** + * Format of the localization file specified with --i18n-file. + */ + i18nFormat?: string; + + /** + * Locale to use for i18n. + */ + i18nLocale?: string; + + /** + * How to handle missing translations for i18n. + */ + i18nMissingTranslation?: string; + + /** + * Extract css from global styles onto css files instead of js ones. + */ + extractCss: boolean; + + /** + * Run build when files change. + */ + watch: boolean; + + /** + * Define the output filename cache-busting hashing mode. + */ + outputHashing: OutputHashing; + + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + + /** + * Delete the output path before building. + */ + deleteOutputPath: boolean; + + /** + * Do not use the real path when resolving modules. + */ + preserveSymlinks: boolean; + + /** + * Extract all licenses in a separate file, in the case of production builds only. + */ + extractLicenses: boolean; + + /** + * Show circular dependency warnings on builds. + */ + showCircularDependencies: boolean; + + /** + * Enables @angular-devkit/build-optimizer optimizations when using the 'aot' option. + */ + buildOptimizer: boolean; + + /** + * Use file name for lazy loaded chunks. + */ + namedChunks: boolean; + + /** + * Enables the use of subresource integrity validation. + */ + subresourceIntegrity: boolean; + + /** + * Generates a service worker config for production builds. + */ + serviceWorker: boolean; + + /** + * Flag to prevent building an app shell. + */ + skipAppShell: boolean; + + /** + * The name of the index HTML file. + */ + index: string; + + /** + * Generates a 'stats.json' file which can be analyzed using tools + * such as: #webpack-bundle-analyzer' or https: //webpack.github.io/analyse. + */ + statsJson: boolean; + + /** + * Run the TypeScript type checker in a forked process. + */ + forkTypeChecker: boolean; + + /** + * List of additional NgModule files that will be lazy loaded. + * Lazy router modules with be discovered automatically. + */ + lazyModules: string[]; + + /** + * Budget thresholds to ensure parts of your application stay within boundaries which you set. + */ + budgets: Budget[]; +} + +export interface AssetPattern { + /** + * The pattern to match. + */ + glob: string; + + /** + * The input path dir in which to apply 'glob'. Defaults to the project root. + */ + input: string; + + /** + * Absolute path within the output. + */ + output: string; +} + +export type ExtraEntryPoint = string | ExtraEntryPointObject; + +export interface ExtraEntryPointObject { + /** + * The file to include. + */ + input: string; + + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + + /** + * If the bundle will be lazy loaded. + */ + lazy: boolean; +} + +export declare type FileReplacement = DeprecatedFileReplacment | CurrentFileReplacement; + +export interface DeprecatedFileReplacment { + /** + * The file that should be replaced. + */ + src: string; + + /** + * The file that should replace. + */ + replaceWith: string; +} + +export interface CurrentFileReplacement { + /** + * The file that should be replaced. + */ + replace: string; + + /** + * The file that should replace. + */ + with: string; +} + +/** + * Define the output filename cache-busting hashing mode. + */ +export enum OutputHashing { + All = 'all', + Bundles = 'bundles', + Media = 'media', + None = 'none', +} + +/** + * Options to pass to style preprocessors + */ +export interface StylePreprocessorOptions { + /** + * Paths to include. Paths will be resolved to project root. + */ + includePaths: string[]; +} + +export interface Budget { + /** + * The type of budget. + */ + type: BudgetType; + + /** + * The name of the bundle. + */ + name: string; + + /** + * The baseline size for comparison. + */ + baseline: string; + + /** + * The maximum threshold for warning relative to the baseline. + */ + maximumWarning: string; + + /** + * The maximum threshold for error relative to the baseline. + */ + maximumError: string; + + /** + * The minimum threshold for warning relative to the baseline. + */ + minimumWarning: string; + + /** + * The minimum threshold for error relative to the baseline. + */ + minimumError: string; + + /** + * The threshold for warning relative to the baseline (min & max). + */ + warning: string; + + /** + * The threshold for error relative to the baseline (min & max). + */ + error: string; +} + +export enum BudgetType { + Initial = 'initial', + All = 'all', + Any = 'any', + AllScript = 'allScript', + AnyScript = 'anyScript', + Bundle = 'bundle', +} diff --git a/packages/angular_devkit/build_webpack/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json similarity index 61% rename from packages/angular_devkit/build_webpack/src/browser/schema.json rename to packages/angular_devkit/build_angular/src/browser/schema.json index 8e30ac5d78..ad40b49955 100644 --- a/packages/angular_devkit/build_webpack/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -10,15 +10,6 @@ "$ref": "#/definitions/assetPattern" } }, - "platform": { - "type": "string", - "description": "The runtime platform of the app.", - "default": "browser", - "enum": [ - "browser", - "server" - ] - }, "main": { "type": "string", "description": "The name of the main entry-point file." @@ -29,7 +20,6 @@ }, "tsConfig": { "type": "string", - "default": "tsconfig.app.json", "description": "The name of the TypeScript configuration file." }, "scripts": { @@ -49,7 +39,7 @@ } }, "stylePreprocessorOptions": { - "description": "Options to pass to style preprocessors", + "description": "Options to pass to style preprocessors.", "type": "object", "properties": { "includePaths": { @@ -63,14 +53,18 @@ }, "additionalProperties": false }, - "optimizationLevel": { - "type": "number", + "optimization": { + "type": "boolean", "description": "Defines the optimization level of the build.", - "default": 0 + "default": false }, - "environment": { - "type": "string", - "description": "Defines the build environment." + "fileReplacements": { + "description": "Replace files with other files in the build.", + "type": "array", + "items": { + "$ref": "#/definitions/fileReplacement" + }, + "default": [] }, "outputPath": { "type": "string", @@ -162,7 +156,7 @@ }, "deleteOutputPath": { "type": "boolean", - "description": "delete-output-path", + "description": "Delete the output path before building.", "default": true }, "preserveSymlinks": { @@ -195,15 +189,6 @@ "description": "Enables the use of subresource integrity validation.", "default": false }, - "bundleDependencies": { - "type": "string", - "description": "Available on server platform only. Which external dependencies to bundle into the module. By default, all of node_modules will be kept as requires.", - "default": "none", - "enum": [ - "none", - "all" - ] - }, "serviceWorker": { "type": "boolean", "description": "Generates a service worker config for production builds.", @@ -222,6 +207,27 @@ "type": "boolean", "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https: //webpack.github.io/analyse.", "default": false + }, + "forkTypeChecker": { + "type": "boolean", + "description": "Run the TypeScript type checker in a forked process.", + "default": true + }, + "lazyModules": { + "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules with be discovered automatically.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "budgets": { + "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", + "type": "array", + "items": { + "$ref": "#/definitions/budget" + }, + "default": [] } }, "additionalProperties": false, @@ -241,43 +247,138 @@ }, "input": { "type": "string", - "description": "The input path dir in which to apply 'glob'.", - "default": "./" + "description": "The input path dir in which to apply 'glob'. Defaults to the project root." }, "output": { "type": "string", - "description": "The output path, relative to 'outputPath'.", - "default": "./" - }, - "allowOutsideOutDir": { - "type": "boolean", - "description": "Allow assets to be copied outside the outDir.", - "default": false + "description": "Absolute path within the output." } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "glob", + "input", + "output" + ] + }, + "fileReplacement": { + "oneOf": [ + { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "replaceWith": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "src", + "replaceWith" + ] + }, + { + "type": "object", + "properties": { + "replace": { + "type": "string" + }, + "with": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "replace", + "with" + ] + } + ] }, "extraEntryPoint": { + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include." + }, + "bundleName": { + "type": "string", + "description": "The bundle name for this extra entry point." + }, + "lazy": { + "type": "boolean", + "description": "If the bundle will be lazy loaded.", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "input" + ] + }, + { + "type": "string", + "description": "The file to include." + } + ] + }, + "budget": { "type": "object", "properties": { - "input": { + "type": { "type": "string", - "description": "The file to include." + "description": "The type of budget.", + "enum": [ + "all", + "allScript", + "any", + "anyScript", + "bundle", + "initial" + ] }, - "output": { + "name": { + "type": "string", + "description": "The name of the bundle." + }, + "baseline": { + "type": "string", + "description": "The baseline size for comparison." + }, + "maximumWarning": { + "type": "string", + "description": "The maximum threshold for warning relative to the baseline." + }, + "maximumError": { "type": "string", - "description": "The output path and filename, relative to 'outputPath'." + "description": "The maximum threshold for error relative to the baseline." }, - "lazy": { - "type": "boolean", - "description": "Allow assets to be copied outside the outDir.", - "default": false + "minimumWarning": { + "type": "string", + "description": "The minimum threshold for warning relative to the baseline." + }, + "minimumError": { + "type": "string", + "description": "The minimum threshold for error relative to the baseline." + }, + "warning": { + "type": "string", + "description": "The threshold for warning relative to the baseline (min & max)." + }, + "error": { + "type": "string", + "description": "The threshold for error relative to the baseline (min & max)." } }, "additionalProperties": false, "required": [ - "input" + "type" ] } } -} +} \ No newline at end of file diff --git a/packages/angular_devkit/build_webpack/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts similarity index 75% rename from packages/angular_devkit/build_webpack/src/dev-server/index.ts rename to packages/angular_devkit/build_angular/src/dev-server/index.ts index 4fe0f9573f..96e8843023 100644 --- a/packages/angular_devkit/build_webpack/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -6,12 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import { BuildEvent, Builder, BuilderContext, Target } from '@angular-devkit/architect'; -import { getSystemPath, tags } from '@angular-devkit/core'; +import { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, resolve, tags, virtualFs } from '@angular-devkit/core'; import { existsSync, readFileSync } from 'fs'; +import * as fs from 'fs'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import { concatMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { concatMap, map, tap } from 'rxjs/operators'; import * as url from 'url'; import * as webpack from 'webpack'; import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils'; @@ -21,10 +27,9 @@ import { statsToString, statsWarningsToString, } from '../angular-cli-files/utilities/stats'; -import { - BrowserBuilder, - BrowserBuilderOptions, -} from '../browser/'; +import { BrowserBuilder } from '../browser/'; +import { BrowserBuilderSchema } from '../browser/schema'; +import { addFileReplacements } from '../utils'; const opn = require('opn'); const WebpackDevServer = require('webpack-dev-server'); @@ -65,8 +70,7 @@ interface WebpackDevServerConfigurationOptions { }; publicPath?: string; headers?: { [key: string]: string }; - stats?: { [key: string]: boolean } | string; - inline: boolean; + stats?: { [key: string]: boolean } | string | boolean; https?: boolean; key?: string; cert?: string; @@ -79,22 +83,34 @@ export class DevServerBuilder implements Builder { constructor(public context: BuilderContext) { } - run(target: Target): Observable { - const root = getSystemPath(target.root); - const options = target.options; + run(builderConfig: BuilderConfiguration): Observable { + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); + const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); + let browserOptions: BrowserBuilderSchema; return checkPort(options.port, options.host).pipe( - concatMap((port) => { - options.port = port; - - return this._getBrowserOptions(options); - }), - concatMap((browserOptions) => new Observable(obs => { + tap((port) => options.port = port), + concatMap(() => this._getBrowserOptions(options)), + tap((opts) => browserOptions = opts), + concatMap(() => addFileReplacements(root, host, browserOptions.fileReplacements)), + concatMap(() => new Observable(obs => { const browserBuilder = new BrowserBuilder(this.context); - const webpackConfig = browserBuilder.buildWebpackConfig(root, browserOptions); + const webpackConfig = browserBuilder.buildWebpackConfig( + root, projectRoot, host, browserOptions); const webpackCompiler = webpack(webpackConfig); const statsConfig = getWebpackStatsConfig(browserOptions.verbose); - const webpackDevServerConfig = this._buildServerConfig(root, options, browserOptions); + + let webpackDevServerConfig: WebpackDevServerConfigurationOptions; + try { + webpackDevServerConfig = this._buildServerConfig( + root, projectRoot, options, browserOptions); + } catch (err) { + obs.error(err); + + return; + } // Resolve public host and client address. let clientAddress = `${options.ssl ? 'https' : 'http'}://0.0.0.0:0`; @@ -126,15 +142,16 @@ export class DevServerBuilder implements Builder { // There's no option to turn off file watching in webpack-dev-server, but // we can override the file watcher instead. webpackConfig.plugins.unshift({ - apply: (compiler: any) => { // tslint:disable-line:no-any - compiler.plugin('after-environment', () => { + // tslint:disable-next-line:no-any + apply: (compiler: any) => { + compiler.hooks.afterEnvironment.tap('angular-cli', () => { compiler.watchFileSystem = { watch: () => { } }; }); }, }); } - if (browserOptions.optimizationLevel > 0) { + if (browserOptions.optimization) { this.context.logger.error(tags.stripIndents` **************************************************************************************** This is a simple server for use in testing or debugging Angular applications locally. @@ -153,8 +170,11 @@ export class DevServerBuilder implements Builder { `); const server = new WebpackDevServer(webpackCompiler, webpackDevServerConfig); - if (!browserOptions.verbose) { - webpackCompiler.plugin('done', (stats: any) => { // tslint:disable-line:no-any + + let first = true; + // tslint:disable-next-line:no-any + (webpackCompiler as any).hooks.done.tap('angular-cli', (stats: webpack.Stats) => { + if (!browserOptions.verbose) { const json = stats.toJson(statsConfig); this.context.logger.info(statsToString(json, statsConfig)); if (stats.hasWarnings()) { @@ -163,22 +183,24 @@ export class DevServerBuilder implements Builder { if (stats.hasErrors()) { this.context.logger.info(statsErrorsToString(json, statsConfig)); } - obs.next(); - }); - } + } + obs.next({ success: true }); + + if (first && options.open) { + first = false; + opn(serverAddress + webpackDevServerConfig.publicPath); + } + }); const httpServer = server.listen( options.port, options.host, - (err: any, _stats: any) => { // tslint:disable-line:no-any + (err: any) => { // tslint:disable-line:no-any if (err) { obs.error(err); - - return; - } else if (options.open) { - opn(serverAddress + webpackDevServerConfig.publicPath); } - }); + }, + ); // Node 8 has a keepAliveTimeout bug which doesn't respect active connections. // Connections will end after ~5 seconds (arbitrary), often not letting the full download @@ -197,10 +219,12 @@ export class DevServerBuilder implements Builder { } private _buildServerConfig( - root: string, + root: Path, + projectRoot: Path, options: DevServerBuilderOptions, - browserOptions: BrowserBuilderOptions, + browserOptions: BrowserBuilderSchema, ) { + const systemRoot = getSystemPath(root); if (options.disableHostCheck) { this.context.logger.warn(tags.oneLine` WARNING: Running a server with --disable-host-check is a security risk. @@ -214,19 +238,18 @@ export class DevServerBuilder implements Builder { const config: WebpackDevServerConfigurationOptions = { headers: { 'Access-Control-Allow-Origin': '*' }, historyApiFallback: { - index: `${servePath}/${browserOptions.index}`, + index: `${servePath}/${path.basename(browserOptions.index)}`, disableDotRule: true, htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], }, - stats: 'none', // We have our own stats function. - inline: true, - compress: browserOptions.optimizationLevel > 0, + stats: browserOptions.verbose ? getWebpackStatsConfig(browserOptions.verbose) : false, + compress: browserOptions.optimization, watchOptions: { poll: browserOptions.poll, }, https: options.ssl, overlay: { - errors: browserOptions.optimizationLevel === 0, + errors: !browserOptions.optimization, warnings: false, }, contentBase: false, @@ -237,11 +260,11 @@ export class DevServerBuilder implements Builder { }; if (options.ssl) { - this._addSslConfig(root, options, config); + this._addSslConfig(systemRoot, options, config); } if (options.proxyConfig) { - this._addProxyConfig(root, options, config); + this._addProxyConfig(systemRoot, options, config); } return config; @@ -249,15 +272,19 @@ export class DevServerBuilder implements Builder { private _addLiveReload( options: DevServerBuilderOptions, - browserOptions: BrowserBuilderOptions, + browserOptions: BrowserBuilderSchema, webpackConfig: any, // tslint:disable-line:no-any clientAddress: string, ) { // This allows for live reload of page when changes are made to repo. // https://webpack.js.org/configuration/dev-server/#devserver-inline - const entryPoints = [ - `webpack-dev-server/client?${clientAddress}`, - ]; + let webpackDevServerPath; + try { + webpackDevServerPath = require.resolve('webpack-dev-server/client'); + } catch { + throw new Error('The "webpack-dev-server" package could not be found.'); + } + const entryPoints = [`${webpackDevServerPath}?${clientAddress}`]; if (options.hmr) { const webpackHmrLink = 'https://webpack.js.org/guides/hot-module-replacement'; @@ -294,13 +321,17 @@ export class DevServerBuilder implements Builder { ) { let sslKey: string | undefined = undefined; let sslCert: string | undefined = undefined; - const keyPath = path.resolve(root, options.sslKey as string); - if (existsSync(keyPath)) { - sslKey = readFileSync(keyPath, 'utf-8'); + if (options.sslKey) { + const keyPath = path.resolve(root, options.sslKey as string); + if (existsSync(keyPath)) { + sslKey = readFileSync(keyPath, 'utf-8'); + } } - const certPath = path.resolve(root, options.sslCert as string); - if (existsSync(certPath)) { - sslCert = readFileSync(certPath, 'utf-8'); + if (options.sslCert) { + const certPath = path.resolve(root, options.sslCert as string); + if (existsSync(certPath)) { + sslCert = readFileSync(certPath, 'utf-8'); + } } config.https = true; @@ -326,7 +357,7 @@ export class DevServerBuilder implements Builder { config.proxy = proxyConfig; } - private _buildServePath(options: DevServerBuilderOptions, browserOptions: BrowserBuilderOptions) { + private _buildServePath(options: DevServerBuilderOptions, browserOptions: BrowserBuilderSchema) { let servePath = options.servePath; if (!servePath && servePath !== '') { const defaultServePath = @@ -386,17 +417,18 @@ export class DevServerBuilder implements Builder { } private _getBrowserOptions(options: DevServerBuilderOptions) { + const architect = this.context.architect; const [project, target, configuration] = options.browserTarget.split(':'); // Override browser build watch setting. const overrides = { watch: options.watch }; - let browserTarget: Target; - - const browserTargetOptions = { project, target, configuration, overrides }; - browserTarget = this.context.architect.getTarget(browserTargetOptions); + const browserTargetSpec = { project, target, configuration, overrides }; + const builderConfig = architect.getBuilderConfiguration( + browserTargetSpec); - return this.context.architect.getBuilderDescription(browserTarget).pipe( + return architect.getBuilderDescription(builderConfig).pipe( concatMap(browserDescription => - this.context.architect.validateBuilderOptions(browserTarget, browserDescription)), + architect.validateBuilderOptions(builderConfig, browserDescription)), + map(browserConfig => browserConfig.options), ); } } diff --git a/packages/angular_devkit/build_webpack/src/dev-server/schema.json b/packages/angular_devkit/build_angular/src/dev-server/schema.json similarity index 98% rename from packages/angular_devkit/build_webpack/src/dev-server/schema.json rename to packages/angular_devkit/build_angular/src/dev-server/schema.json index 56d6902bd2..7cff55f8a6 100644 --- a/packages/angular_devkit/build_webpack/src/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/dev-server/schema.json @@ -37,7 +37,8 @@ "open": { "type": "boolean", "description": "Opens the url in default browser.", - "default": false + "default": false, + "alias": "o" }, "liveReload": { "type": "boolean", diff --git a/packages/angular_devkit/build_webpack/src/extract-i18n/index.ts b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts similarity index 51% rename from packages/angular_devkit/build_webpack/src/extract-i18n/index.ts rename to packages/angular_devkit/build_angular/src/extract-i18n/index.ts index 59910249d4..e6fdc0ebc7 100644 --- a/packages/angular_devkit/build_webpack/src/extract-i18n/index.ts +++ b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts @@ -5,17 +5,30 @@ * Use 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 { BuildEvent, Builder, BuilderContext, Target } from '@angular-devkit/architect'; -import { getSystemPath } from '@angular-devkit/core'; +import { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import * as fs from 'fs'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import { concatMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { concatMap, map } from 'rxjs/operators'; import * as webpack from 'webpack'; +import { WebpackConfigOptions } from '../angular-cli-files/models/build-options'; +import { + getAotConfig, + getCommonConfig, + getStylesConfig, +} from '../angular-cli-files/models/webpack-configs'; import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils'; +import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; import { statsErrorsToString, statsWarningsToString } from '../angular-cli-files/utilities/stats'; -import { BrowserBuilder, BrowserBuilderOptions } from '../browser'; +import { BrowserBuilderSchema } from '../browser/schema'; const MemoryFS = require('memory-fs'); +const webpackMerge = require('webpack-merge'); export interface ExtractI18nBuilderOptions { @@ -30,26 +43,25 @@ export class ExtractI18nBuilder implements Builder { constructor(public context: BuilderContext) { } - run(target: Target): Observable { - - const root = getSystemPath(target.root); - const options = target.options; - - + run(builderConfig: BuilderConfiguration): Observable { + const architect = this.context.architect; + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); const [project, targetName, configuration] = options.browserTarget.split(':'); // Override browser build watch setting. const overrides = { watch: false }; - const browserTargetOptions = { project, target: targetName, configuration, overrides }; - const browserTarget = this.context.architect - .getTarget(browserTargetOptions); + const browserTargetSpec = { project, target: targetName, configuration, overrides }; + const browserBuilderConfig = architect.getBuilderConfiguration( + browserTargetSpec); - return this.context.architect.getBuilderDescription(browserTarget).pipe( + return architect.getBuilderDescription(browserBuilderConfig).pipe( concatMap(browserDescription => - this.context.architect.validateBuilderOptions(browserTarget, browserDescription)), + architect.validateBuilderOptions(browserBuilderConfig, browserDescription)), + map(browserBuilderConfig => browserBuilderConfig.options), concatMap((validatedBrowserOptions) => new Observable(obs => { const browserOptions = validatedBrowserOptions; - const browserBuilder = new BrowserBuilder(this.context); // We need to determine the outFile name so that AngularCompiler can retrieve it. let outFile = options.outFile || getI18nOutfile(options.i18nFormat); @@ -59,13 +71,16 @@ export class ExtractI18nBuilder implements Builder { } // Extracting i18n uses the browser target webpack config with some specific options. - const webpackConfig = browserBuilder.buildWebpackConfig(root, { + const webpackConfig = this.buildWebpackConfig(root, projectRoot, { ...browserOptions, - optimizationLevel: 0, + optimization: false, i18nLocale: options.i18nLocale, - i18nOutFormat: options.i18nFormat, - i18nOutFile: outFile, + i18nFormat: options.i18nFormat, + i18nFile: outFile, aot: true, + assets: [], + scripts: [], + styles: [], }); const webpackCompiler = webpack(webpackConfig); @@ -83,10 +98,12 @@ export class ExtractI18nBuilder implements Builder { } if (stats.hasErrors()) { - obs.error(statsErrorsToString(json, statsConfig)); - } else { - obs.complete(); + this.context.logger.error(statsErrorsToString(json, statsConfig)); } + + obs.next({ success: !stats.hasErrors() }); + + obs.complete(); }; try { @@ -98,7 +115,39 @@ export class ExtractI18nBuilder implements Builder { } throw err; } - }))); + })), + ); + } + + buildWebpackConfig( + root: Path, + projectRoot: Path, + options: BrowserBuilderSchema, + ) { + let wco: WebpackConfigOptions; + + const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); + + const tsConfigPath = getSystemPath(normalize(resolve(root, normalize(options.tsConfig)))); + const tsConfig = readTsconfig(tsConfigPath); + + wco = { + root: getSystemPath(root), + projectRoot: getSystemPath(projectRoot), + // TODO: use only this.options, it contains all flags and configs items already. + buildOptions: options, + tsConfig, + tsConfigPath, + supportES2015: false, + }; + + const webpackConfigs: {}[] = [ + getCommonConfig(wco), + getAotConfig(wco, host, true), + getStylesConfig(wco), + ]; + + return webpackMerge(webpackConfigs); } } diff --git a/packages/angular_devkit/build_webpack/src/extract-i18n/schema.json b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json similarity index 97% rename from packages/angular_devkit/build_webpack/src/extract-i18n/schema.json rename to packages/angular_devkit/build_angular/src/extract-i18n/schema.json index 9db5c54059..00dae2cdcb 100644 --- a/packages/angular_devkit/build_webpack/src/extract-i18n/schema.json +++ b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json @@ -29,7 +29,7 @@ "description": "Path where output will be placed." }, "outFile": { - "type": "boolean", + "type": "string", "description": "Name of the file to output." } }, diff --git a/packages/angular_devkit/build_angular/src/index.ts b/packages/angular_devkit/build_angular/src/index.ts new file mode 100644 index 0000000000..ba47eb494b --- /dev/null +++ b/packages/angular_devkit/build_angular/src/index.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// TODO: remove this commented AJV require. +// We don't actually require AJV, but there is a bug with NPM and peer dependencies that is +// whose workaround is to depend on AJV. +// See https://github.com/angular/angular-cli/issues/9691#issuecomment-367322703 for details. +// We need to add a require here to satisfy the dependency checker. +// require('ajv'); + +export * from './app-shell'; +export * from './browser'; +export * from './browser/schema'; +export * from './dev-server'; +export * from './extract-i18n'; +export * from './karma'; +export * from './karma/schema'; +export * from './protractor'; +export * from './server'; +export * from './tslint'; diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts new file mode 100644 index 0000000000..9e44e70ade --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/index.ts @@ -0,0 +1,142 @@ +/** + * @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 { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import * as fs from 'fs'; +import { Observable, of } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies +import { WebpackConfigOptions } from '../angular-cli-files/models/build-options'; +import { + getCommonConfig, + getNonAotTestConfig, + getStylesConfig, + getTestConfig, +} from '../angular-cli-files/models/webpack-configs'; +import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; +import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; +import { CurrentFileReplacement } from '../browser/schema'; +import { addFileReplacements } from '../utils'; +import { KarmaBuilderSchema } from './schema'; +const webpackMerge = require('webpack-merge'); + + +export interface KarmaBuilderOptions extends KarmaBuilderSchema { + fileReplacements: CurrentFileReplacement[]; +} + +export class KarmaBuilder implements Builder { + constructor(public context: BuilderContext) { } + + run(builderConfig: BuilderConfiguration): Observable { + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); + const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); + + return of(null).pipe( + concatMap(() => addFileReplacements(root, host, options.fileReplacements)), + concatMap(() => new Observable(obs => { + const karma = requireProjectModule(getSystemPath(projectRoot), 'karma'); + const karmaConfig = getSystemPath(resolve(root, normalize(options.karmaConfig))); + + // TODO: adjust options to account for not passing them blindly to karma. + // const karmaOptions: any = Object.assign({}, options); + // tslint:disable-next-line:no-any + const karmaOptions: any = { + singleRun: !options.watch, + }; + + // Convert browsers from a string to an array + if (options.browsers) { + karmaOptions.browsers = options.browsers.split(','); + } + + karmaOptions.buildWebpack = { + root: getSystemPath(root), + projectRoot: getSystemPath(projectRoot), + options: options, + webpackConfig: this._buildWebpackConfig(root, projectRoot, host, options), + // Pass onto Karma to emit BuildEvents. + successCb: () => obs.next({ success: true }), + failureCb: () => obs.next({ success: false }), + }; + + // TODO: inside the configs, always use the project root and not the workspace root. + // Until then we pretend the app root is relative (``) but the same as `projectRoot`. + (karmaOptions.buildWebpack.options as any).root = ''; // tslint:disable-line:no-any + + // Assign additional karmaConfig options to the local ngapp config + karmaOptions.configFile = karmaConfig; + + // Complete the observable once the Karma server returns. + const karmaServer = new karma.Server(karmaOptions, () => obs.complete()); + karmaServer.start(); + + // Cleanup, signal Karma to exit. + return () => { + // Karma does not seem to have a way to exit the server gracefully. + // See https://github.com/karma-runner/karma/issues/2867#issuecomment-369912167 + // TODO: make a PR for karma to add `karmaServer.close(code)`, that + // calls `disconnectBrowsers(code);` + // karmaServer.close(); + }; + })), + ); + } + + private _buildWebpackConfig( + root: Path, + projectRoot: Path, + host: virtualFs.Host, + options: KarmaBuilderOptions, + ) { + let wco: WebpackConfigOptions; + + const tsConfigPath = getSystemPath(resolve(root, normalize(options.tsConfig as string))); + const tsConfig = readTsconfig(tsConfigPath); + + const projectTs = requireProjectModule(getSystemPath(projectRoot), 'typescript') as typeof ts; + + const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && tsConfig.options.target !== projectTs.ScriptTarget.ES5; + + const compatOptions: typeof wco['buildOptions'] = { + ...options as {} as typeof wco['buildOptions'], + // Some asset logic inside getCommonConfig needs outputPath to be set. + outputPath: '', + }; + + wco = { + root: getSystemPath(root), + projectRoot: getSystemPath(projectRoot), + // TODO: use only this.options, it contains all flags and configs items already. + buildOptions: compatOptions, + tsConfig, + tsConfigPath, + supportES2015, + }; + + const webpackConfigs: {}[] = [ + getCommonConfig(wco), + getStylesConfig(wco), + getNonAotTestConfig(wco, host), + getTestConfig(wco), + ]; + + return webpackMerge(webpackConfigs); + } +} + +export default KarmaBuilder; diff --git a/packages/angular_devkit/build_angular/src/karma/schema.d.ts b/packages/angular_devkit/build_angular/src/karma/schema.d.ts new file mode 100644 index 0000000000..650e05c3d6 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/schema.d.ts @@ -0,0 +1,35 @@ +/** + * @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 { BrowserBuilderSchema } from '../browser/schema'; + + +// TODO: in TS 2.8 use Extract instead of Pick to make a subset of another type. +export interface KarmaBuilderSchema extends Pick { + /** + * The name of the Karma configuration file.. + */ + karmaConfig: string; + + /** + * Override which browsers tests are run against. + */ + browsers: string; + + /** + * Output a code coverage report. + */ + codeCoverage: boolean; + + /** + * Globs to exclude from code coverage. + */ + codeCoverageExclude: string[]; +} diff --git a/packages/angular_devkit/build_webpack/src/karma/schema.json b/packages/angular_devkit/build_angular/src/karma/schema.json similarity index 61% rename from packages/angular_devkit/build_webpack/src/karma/schema.json rename to packages/angular_devkit/build_angular/src/karma/schema.json index 2297c4ed5d..5b7115c979 100644 --- a/packages/angular_devkit/build_webpack/src/karma/schema.json +++ b/packages/angular_devkit/build_angular/src/karma/schema.json @@ -9,13 +9,11 @@ }, "tsConfig": { "type": "string", - "default": "tsconfig.app.json", "description": "The name of the TypeScript configuration file." }, "karmaConfig": { "type": "string", - "default": "tsconfig.app.json", - "description": "The name of the TypeScript configuration file." + "description": "The name of the Karma configuration file." }, "polyfills": { "type": "string", @@ -94,8 +92,57 @@ }, "codeCoverage": { "type": "boolean", - "description": "Override which browsers tests are run against.", + "description": "Output a code coverage report.", "default": false + }, + "codeCoverageExclude": { + "type": "array", + "description": "Globs to exclude from code coverage.", + "items": { + "type": "string" + }, + "default": [] + }, + "fileReplacements": { + "description": "Replace files with other files in the build.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "replaceWith": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "src", + "replaceWith" + ] + }, + { + "type": "object", + "properties": { + "replace": { + "type": "string" + }, + "with": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "replace", + "with" + ] + } + ] + }, + "default": [] } }, "additionalProperties": false, @@ -114,43 +161,49 @@ }, "input": { "type": "string", - "description": "The input path dir in which to apply 'glob'.", - "default": "./" + "description": "The input path dir in which to apply 'glob'. Defaults to the project root." }, "output": { "type": "string", - "description": "The output path, relative to 'outputPath'.", - "default": "./" - }, - "allowOutsideOutDir": { - "type": "boolean", - "description": "Allow assets to be copied outside the outDir.", - "default": false + "description": "Absolute path within the output." } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "glob", + "input", + "output" + ] }, "extraEntryPoint": { - "type": "object", - "properties": { - "input": { - "type": "string", - "description": "The file to include." + "oneOf": [ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "The file to include." + }, + "bundleName": { + "type": "string", + "description": "The bundle name for this extra entry point." + }, + "lazy": { + "type": "boolean", + "description": "If the bundle will be lazy loaded.", + "default": false + } + }, + "additionalProperties": false, + "required": [ + "input" + ] }, - "output": { + { "type": "string", - "description": "The output path and filename, relative to 'outputPath'." - }, - "lazy": { - "type": "boolean", - "description": "Allow assets to be copied outside the outDir.", - "default": false + "description": "The file to include." } - }, - "additionalProperties": false, - "required": [ - "input" ] } } -} +} \ No newline at end of file diff --git a/packages/angular_devkit/build_webpack/src/protractor/index.ts b/packages/angular_devkit/build_angular/src/protractor/index.ts similarity index 62% rename from packages/angular_devkit/build_webpack/src/protractor/index.ts rename to packages/angular_devkit/build_angular/src/protractor/index.ts index 0601264a92..5af4b7fd2f 100644 --- a/packages/angular_devkit/build_webpack/src/protractor/index.ts +++ b/packages/angular_devkit/build_angular/src/protractor/index.ts @@ -9,17 +9,13 @@ import { BuildEvent, Builder, + BuilderConfiguration, BuilderContext, BuilderDescription, - Target, } from '@angular-devkit/architect'; -import { getSystemPath, tags } from '@angular-devkit/core'; -import { resolve } from 'path'; -import { Observable } from 'rxjs/Observable'; -import { empty } from 'rxjs/observable/empty'; -import { fromPromise } from 'rxjs/observable/fromPromise'; -import { of } from 'rxjs/observable/of'; -import { concatMap } from 'rxjs/operators'; +import { Path, getSystemPath, normalize, resolve, tags } from '@angular-devkit/core'; +import { Observable, from, of } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; import * as url from 'url'; import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; import { DevServerBuilderOptions } from '../dev-server'; @@ -42,40 +38,43 @@ export class ProtractorBuilder implements Builder { constructor(public context: BuilderContext) { } - run(target: Target): Observable { + run(builderConfig: BuilderConfiguration): Observable { - const root = getSystemPath(target.root); - const options = target.options; + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); + // const projectSystemRoot = getSystemPath(projectRoot); - return (options.devServerTarget ? this._startDevServer(options) : empty()).pipe( - concatMap(() => options.webdriverUpdate ? this._updateWebdriver(root) : empty()), + // TODO: verify using of(null) to kickstart things is a pattern. + return of(null).pipe( + concatMap(() => options.devServerTarget ? this._startDevServer(options) : of(null)), + concatMap(() => options.webdriverUpdate ? this._updateWebdriver(projectRoot) : of(null)), concatMap(() => this._runProtractor(root, options)), + take(1), ); } + // Note: this method mutates the options argument. private _startDevServer(options: ProtractorBuilderOptions) { + const architect = this.context.architect; const [project, targetName, configuration] = (options.devServerTarget as string).split(':'); // Override browser build watch setting. const overrides = { watch: false, host: options.host, port: options.port }; - const browserTargetOptions = { project, target: targetName, configuration, overrides }; - const devServerTarget = this.context.architect - .getTarget(browserTargetOptions); + const targetSpec = { project, target: targetName, configuration, overrides }; + const builderConfig = architect.getBuilderConfiguration(targetSpec); let devServerDescription: BuilderDescription; let baseUrl: string; - return this.context.architect.getBuilderDescription(devServerTarget).pipe( - concatMap(description => { - devServerDescription = description; - - return this.context.architect.validateBuilderOptions(devServerTarget, - devServerDescription); - }), + return architect.getBuilderDescription(builderConfig).pipe( + tap(description => devServerDescription = description), + concatMap(devServerDescription => architect.validateBuilderOptions( + builderConfig, devServerDescription)), concatMap(() => { // Compute baseUrl from devServerOptions. - if (options.devServerTarget && devServerTarget.options.publicHost) { - let publicHost = devServerTarget.options.publicHost; + if (options.devServerTarget && builderConfig.options.publicHost) { + let publicHost = builderConfig.options.publicHost; if (!/^\w+:\/\//.test(publicHost)) { - publicHost = `${devServerTarget.options.ssl + publicHost = `${builderConfig.options.ssl ? 'https' : 'http'}://${publicHost}`; } @@ -83,9 +82,9 @@ export class ProtractorBuilder implements Builder { baseUrl = url.format(clientUrl); } else if (options.devServerTarget) { baseUrl = url.format({ - protocol: devServerTarget.options.ssl ? 'https' : 'http', + protocol: builderConfig.options.ssl ? 'https' : 'http', hostname: options.host, - port: devServerTarget.options.port.toString(), + port: builderConfig.options.port.toString(), }); } @@ -94,23 +93,23 @@ export class ProtractorBuilder implements Builder { return of(this.context.architect.getBuilder(devServerDescription, this.context)); }), - concatMap(builder => builder.run(devServerTarget)), + concatMap(builder => builder.run(builderConfig)), ); } - private _updateWebdriver(root: string) { + private _updateWebdriver(projectRoot: Path) { // The webdriver-manager update command can only be accessed via a deep import. const webdriverDeepImport = 'webdriver-manager/built/lib/cmds/update'; let webdriverUpdate: any; // tslint:disable-line:no-any try { // When using npm, webdriver is within protractor/node_modules. - webdriverUpdate = requireProjectModule(root, + webdriverUpdate = requireProjectModule(getSystemPath(projectRoot), `protractor/node_modules/${webdriverDeepImport}`); } catch (e) { try { // When using yarn, webdriver is found as a root module. - webdriverUpdate = requireProjectModule(root, webdriverDeepImport); + webdriverUpdate = requireProjectModule(getSystemPath(projectRoot), webdriverDeepImport); } catch (e) { throw new Error(tags.stripIndents` Cannot automatically find webdriver-manager to update. @@ -121,14 +120,14 @@ export class ProtractorBuilder implements Builder { // run `webdriver-manager update --standalone false --gecko false --quiet` // if you change this, update the command comment in prev line, and in `eject` task - return fromPromise(webdriverUpdate.program.run({ + return from(webdriverUpdate.program.run({ standalone: false, gecko: false, quiet: true, })); } - private _runProtractor(root: string, options: ProtractorBuilderOptions): Observable { + private _runProtractor(root: Path, options: ProtractorBuilderOptions): Observable { const additionalProtractorConfig = { elementExplorer: options.elementExplorer, baseUrl: options.baseUrl, @@ -143,7 +142,10 @@ export class ProtractorBuilder implements Builder { root, 'protractor/built/launcher', 'init', - [resolve(root, options.protractorConfig), additionalProtractorConfig], + [ + getSystemPath(resolve(root, normalize(options.protractorConfig))), + additionalProtractorConfig, + ], ); } } diff --git a/packages/angular_devkit/build_webpack/src/protractor/schema.json b/packages/angular_devkit/build_angular/src/protractor/schema.json similarity index 100% rename from packages/angular_devkit/build_webpack/src/protractor/schema.json rename to packages/angular_devkit/build_angular/src/protractor/schema.json diff --git a/packages/angular_devkit/build_angular/src/server/index.ts b/packages/angular_devkit/build_angular/src/server/index.ts new file mode 100644 index 0000000000..c147bb74d6 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/server/index.ts @@ -0,0 +1,172 @@ +/** + * @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 { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import { Stats } from 'fs'; +import { Observable, concat, of } from 'rxjs'; +import { concatMap, last } from 'rxjs/operators'; +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies +import * as webpack from 'webpack'; +import { WebpackConfigOptions } from '../angular-cli-files/models/build-options'; +import { + getAotConfig, + getCommonConfig, + getNonAotConfig, + getServerConfig, + getStylesConfig, +} from '../angular-cli-files/models/webpack-configs'; +import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils'; +import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; +import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; +import { + statsErrorsToString, + statsToString, + statsWarningsToString, +} from '../angular-cli-files/utilities/stats'; +import { BuildWebpackServerSchema } from './schema'; +const webpackMerge = require('webpack-merge'); + + +export class ServerBuilder implements Builder { + + constructor(public context: BuilderContext) { } + + run(builderConfig: BuilderConfiguration): Observable { + const options = builderConfig.options; + const root = this.context.workspace.root; + const projectRoot = resolve(root, builderConfig.root); + + // TODO: verify using of(null) to kickstart things is a pattern. + return of(null).pipe( + concatMap(() => options.deleteOutputPath + ? this._deleteOutputDir(root, normalize(options.outputPath)) + : of(null)), + concatMap(() => new Observable(obs => { + // Ensure Build Optimizer is only used with AOT. + let webpackConfig; + try { + webpackConfig = this.buildWebpackConfig(root, projectRoot, options); + } catch (e) { + // TODO: why do I have to catch this error? I thought throwing inside an observable + // always got converted into an error. + obs.error(e); + + return; + } + const webpackCompiler = webpack(webpackConfig); + const statsConfig = getWebpackStatsConfig(options.verbose); + + const callback: webpack.compiler.CompilerCallback = (err, stats) => { + if (err) { + return obs.error(err); + } + + const json = stats.toJson(statsConfig); + if (options.verbose) { + this.context.logger.info(stats.toString(statsConfig)); + } else { + this.context.logger.info(statsToString(json, statsConfig)); + } + + if (stats.hasWarnings()) { + this.context.logger.warn(statsWarningsToString(json, statsConfig)); + } + if (stats.hasErrors()) { + this.context.logger.error(statsErrorsToString(json, statsConfig)); + } + + obs.next({ success: !stats.hasErrors() }); + obs.complete(); + }; + + try { + webpackCompiler.run(callback); + } catch (err) { + if (err) { + this.context.logger.error( + '\nAn error occured during the build:\n' + ((err && err.stack) || err)); + } + throw err; + } + })), + ); + } + + buildWebpackConfig(root: Path, projectRoot: Path, options: BuildWebpackServerSchema) { + let wco: WebpackConfigOptions; + + // TODO: make target defaults into configurations instead + // options = this.addTargetDefaults(options); + + const tsConfigPath = getSystemPath(normalize(resolve(root, normalize(options.tsConfig)))); + const tsConfig = readTsconfig(tsConfigPath); + + const projectTs = requireProjectModule(getSystemPath(projectRoot), 'typescript') as typeof ts; + + const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && tsConfig.options.target !== projectTs.ScriptTarget.ES5; + + const buildOptions: typeof wco['buildOptions'] = { + ...options as {} as typeof wco['buildOptions'], + }; + + wco = { + root: getSystemPath(root), + projectRoot: getSystemPath(projectRoot), + // TODO: use only this.options, it contains all flags and configs items already. + buildOptions: { + ...buildOptions, + aot: true, + platform: 'server', + scripts: [], + styles: [], + }, + tsConfig, + tsConfigPath, + supportES2015, + }; + + const webpackConfigs: {}[] = [ + getCommonConfig(wco), + getServerConfig(wco), + getStylesConfig(wco), + ]; + + if (wco.buildOptions.main || wco.buildOptions.polyfills) { + const typescriptConfigPartial = wco.buildOptions.aot + ? getAotConfig(wco, this.context.host as virtualFs.Host) + : getNonAotConfig(wco, this.context.host as virtualFs.Host); + webpackConfigs.push(typescriptConfigPartial); + } + + return webpackMerge(webpackConfigs); + } + + private _deleteOutputDir(root: Path, outputPath: Path) { + const resolvedOutputPath = resolve(root, outputPath); + if (resolvedOutputPath === root) { + throw new Error('Output path MUST not be project root directory!'); + } + + return this.context.host.exists(resolvedOutputPath).pipe( + concatMap(exists => exists + // TODO: remove this concat once host ops emit an event. + ? concat(this.context.host.delete(resolvedOutputPath), of(null)).pipe(last()) + // ? of(null) + : of(null)), + ); + } +} + +export default ServerBuilder; diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts new file mode 100644 index 0000000000..d4daa6557d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface BuildWebpackServerSchema { + /** + * The name of the TypeScript configuration file. + */ + tsConfig: string; + /** + * Output sourcemaps. + */ + sourceMap?: boolean; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Use a separate bundle containing code used across multiple bundles. + */ + commonChunk?: boolean; + /** + * Show circular dependency warnings on builds. + */ + showCircularDependencies?: boolean; + /** + * Use a separate bundle containing only vendor libraries. + */ + vendorChunk?: boolean; + /** + * Output in-file eval sourcemaps. + */ + evalSourceMap?: boolean; + /** + * Path where output will be placed. + */ + outputPath: string; + /** + * Generates a 'stats.json' file which can be analyzed using tools such as: + * #webpack-bundle-analyzer' or https: //webpack.github.io/analyse. + */ + statsJson?: boolean; + /** + * How to handle missing translations for i18n. + */ + i18nMissingTranslation?: string; + /** + * Available on server platform only. Which external dependencies to bundle into the module. + * By default, all of node_modules will be kept as requires. + */ + bundleDependencies?: BundleDependencies; + /** + * Defines the optimization level of the build. + */ + optimization?: boolean; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * delete-output-path + */ + deleteOutputPath?: boolean; + /** + * List of additional NgModule files that will be lazy loaded. Lazy router modules with be + * discovered automatically. + */ + lazyModules?: string[]; + /** + * Defines the build environment. + */ + environment?: string; + /** + * Define the output filename cache-busting hashing mode. + */ + outputHashing?: OutputHashing; + /** + * Extract all licenses in a separate file, in the case of production builds only. + */ + extractLicenses?: boolean; + /** + * Format of the localization file specified with --i18n-file. + */ + i18nFormat?: string; + /** + * Locale to use for i18n. + */ + i18nLocale?: string; + /** + * Run the TypeScript type checker in a forked process. + */ + forkTypeChecker?: boolean; + /** + * The name of the main entry-point file. + */ + main: string; + /** + * Localization file to use for i18n. + */ + i18nFile?: string; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + /** + * Do not use the real path when resolving modules. + */ + preserveSymlinks?: boolean; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; +} + +/** + * Available on server platform only. Which external dependencies to bundle into the module. + * By default, all of node_modules will be kept as requires. + */ +export enum BundleDependencies { + All = 'all', + None = 'none', +} + +/** + * Define the output filename cache-busting hashing mode. + */ +export enum OutputHashing { + All = 'all', + Bundles = 'bundles', + Media = 'media', + None = 'none', +} + +/** + * Options to pass to style preprocessors + */ +export interface StylePreprocessorOptions { + /** + * Paths to include. Paths will be resolved to project root. + */ + includePaths?: string[]; +} diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json new file mode 100644 index 0000000000..42b582a164 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "BuildAngularWebpackServerSchema", + "title": "Angular Webpack Architect Builder Schema", + "properties": { + "main": { + "type": "string", + "description": "The name of the main entry-point file." + }, + "tsConfig": { + "type": "string", + "default": "tsconfig.app.json", + "description": "The name of the TypeScript configuration file." + }, + "stylePreprocessorOptions": { + "description": "Options to pass to style preprocessors", + "type": "object", + "properties": { + "includePaths": { + "description": "Paths to include. Paths will be resolved to project root.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false + }, + "optimization": { + "type": "boolean", + "description": "Defines the optimization level of the build.", + "default": false + }, + "environment": { + "type": "string", + "description": "Defines the build environment." + }, + "outputPath": { + "type": "string", + "description": "Path where output will be placed." + }, + "sourceMap": { + "type": "boolean", + "description": "Output sourcemaps.", + "default": true + }, + "evalSourceMap": { + "type": "boolean", + "description": "Output in-file eval sourcemaps.", + "default": false + }, + "vendorChunk": { + "type": "boolean", + "description": "Use a separate bundle containing only vendor libraries.", + "default": true + }, + "commonChunk": { + "type": "boolean", + "description": "Use a separate bundle containing code used across multiple bundles.", + "default": true + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging.", + "default": false + }, + "progress": { + "type": "boolean", + "description": "Log progress to the console while building.", + "default": true + }, + "i18nFile": { + "type": "string", + "description": "Localization file to use for i18n." + }, + "i18nFormat": { + "type": "string", + "description": "Format of the localization file specified with --i18n-file." + }, + "i18nLocale": { + "type": "string", + "description": "Locale to use for i18n." + }, + "i18nMissingTranslation": { + "type": "string", + "description": "How to handle missing translations for i18n." + }, + "outputHashing": { + "type": "string", + "description": "Define the output filename cache-busting hashing mode.", + "default": "none", + "enum": [ + "none", + "all", + "media", + "bundles" + ] + }, + "deleteOutputPath": { + "type": "boolean", + "description": "delete-output-path", + "default": true + }, + "preserveSymlinks": { + "type": "boolean", + "description": "Do not use the real path when resolving modules.", + "default": false + }, + "extractLicenses": { + "type": "boolean", + "description": "Extract all licenses in a separate file, in the case of production builds only.", + "default": true + }, + "showCircularDependencies": { + "type": "boolean", + "description": "Show circular dependency warnings on builds.", + "default": true + }, + "namedChunks": { + "type": "boolean", + "description": "Use file name for lazy loaded chunks.", + "default": true + }, + "bundleDependencies": { + "type": "string", + "description": "Available on server platform only. Which external dependencies to bundle into the module. By default, all of node_modules will be kept as requires.", + "default": "none", + "enum": [ + "none", + "all" + ] + }, + "statsJson": { + "type": "boolean", + "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https: //webpack.github.io/analyse.", + "default": false + }, + "forkTypeChecker": { + "type": "boolean", + "description": "Run the TypeScript type checker in a forked process.", + "default": true + }, + "lazyModules": { + "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules with be discovered automatically.", + "type": "array", + "items": { + "type": "string" + }, + "default": [] + } + }, + "additionalProperties": false, + "required": [ + "outputPath", + "main", + "tsConfig" + ] +} \ No newline at end of file diff --git a/packages/angular_devkit/build_webpack/src/tslint/index.ts b/packages/angular_devkit/build_angular/src/tslint/index.ts similarity index 52% rename from packages/angular_devkit/build_webpack/src/tslint/index.ts rename to packages/angular_devkit/build_angular/src/tslint/index.ts index a882035e59..77e29ef17f 100644 --- a/packages/angular_devkit/build_webpack/src/tslint/index.ts +++ b/packages/angular_devkit/build_angular/src/tslint/index.ts @@ -6,13 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import { BuildEvent, Builder, BuilderContext, Target } from '@angular-devkit/architect'; -import { getSystemPath } from '@angular-devkit/core'; +import { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { getSystemPath, resolve } from '@angular-devkit/core'; import { readFileSync } from 'fs'; import * as glob from 'glob'; import { Minimatch } from 'minimatch'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import * as tslint from 'tslint'; // tslint:disable-line:no-implicit-dependencies import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; @@ -20,8 +25,8 @@ import { stripBom } from '../angular-cli-files/utilities/strip-bom'; export interface TslintBuilderOptions { - tslintConfig: string; - tsConfig?: string; + tslintConfig?: string; + tsConfig?: string | string[]; fix: boolean; typeCheck: boolean; force: boolean; @@ -35,52 +40,50 @@ export class TslintBuilder implements Builder { constructor(public context: BuilderContext) { } - run(target: Target): Observable { + run(builderConfig: BuilderConfiguration): Observable { - const root = getSystemPath(target.root); - const options = target.options; + const root = this.context.workspace.root; + const systemRoot = getSystemPath(root); + const projectRoot = resolve(root, builderConfig.root); + const options = builderConfig.options; if (!options.tsConfig && options.typeCheck) { throw new Error('A "project" must be specified to enable type checking.'); } return new Observable(obs => { - const projectTslint = requireProjectModule(root, 'tslint') as typeof tslint; + const projectTslint = requireProjectModule( + getSystemPath(projectRoot), 'tslint') as typeof tslint; + const tslintConfigPath = options.tslintConfig + ? path.resolve(systemRoot, options.tslintConfig) + : null; const Linter = projectTslint.Linter; - const Configuration = projectTslint.Configuration; - let program: ts.Program | undefined = undefined; + let result; if (options.tsConfig) { - program = Linter.createProgram(path.resolve(root, options.tsConfig)); - } - - const files = getFilesToLint(options, Linter, program); - const lintOptions = { - fix: options.fix, - formatter: options.format, - }; - - const linter = new Linter(lintOptions, program); - - let lastDirectory; - let configLoad; - for (const file of files) { - const contents = getFileContents(file, options, program); - - // Only check for a new tslint config if path changes - const currentDirectory = path.dirname(file); - if (currentDirectory !== lastDirectory) { - configLoad = Configuration.findConfiguration( - path.resolve(root, options.tslintConfig), file); - lastDirectory = currentDirectory; - } - - if (contents && configLoad) { - linter.lint(file, contents, configLoad.results); + const tsConfigs = Array.isArray(options.tsConfig) ? options.tsConfig : [options.tsConfig]; + + for (const tsConfig of tsConfigs) { + const program = Linter.createProgram(path.resolve(systemRoot, tsConfig)); + const partial = lint(projectTslint, systemRoot, tslintConfigPath, options, program); + if (result == undefined) { + result = partial; + } else { + result.errorCount += partial.errorCount; + result.warningCount += partial.warningCount; + result.failures = result.failures.concat(partial.failures); + if (partial.fixes) { + result.fixes = result.fixes ? result.fixes.concat(partial.fixes) : partial.fixes; + } + } } + } else { + result = lint(projectTslint, systemRoot, tslintConfigPath, options); } - const result = linter.getResult(); + if (result == undefined) { + throw new Error('Invalid lint configuration. Nothing to lint.'); + } if (!options.silent) { const Formatter = projectTslint.findFormatter(options.format); @@ -112,12 +115,54 @@ export class TslintBuilder implements Builder { this.context.logger.info('All files pass linting.'); } - return options.force || result.errorCount === 0 ? obs.complete() : obs.error(); + const success = options.force || result.errorCount === 0; + obs.next({ success }); + + return obs.complete(); }); } } +function lint( + projectTslint: typeof tslint, + systemRoot: string, + tslintConfigPath: string | null, + options: TslintBuilderOptions, + program?: ts.Program, +) { + const Linter = projectTslint.Linter; + const Configuration = projectTslint.Configuration; + + const files = getFilesToLint(systemRoot, options, Linter, program); + const lintOptions = { + fix: options.fix, + formatter: options.format, + }; + + const linter = new Linter(lintOptions, program); + + let lastDirectory; + let configLoad; + for (const file of files) { + const contents = getFileContents(file, options, program); + + // Only check for a new tslint config if the path changes. + const currentDirectory = path.dirname(file); + if (currentDirectory !== lastDirectory) { + configLoad = Configuration.findConfiguration(tslintConfigPath, file); + lastDirectory = currentDirectory; + } + + if (contents && configLoad) { + linter.lint(file, contents, configLoad.results); + } + } + + return linter.getResult(); +} + function getFilesToLint( + root: string, options: TslintBuilderOptions, linter: typeof tslint.Linter, program?: ts.Program, @@ -126,8 +171,9 @@ function getFilesToLint( if (options.files.length > 0) { return options.files - .map(file => glob.sync(file, { ignore, nodir: true })) - .reduce((prev, curr) => prev.concat(curr), []); + .map(file => glob.sync(file, { cwd: root, ignore, nodir: true })) + .reduce((prev, curr) => prev.concat(curr), []) + .map(file => path.join(root, file)); } if (!program) { @@ -158,7 +204,8 @@ function getFileContents( throw new Error(message); } - return undefined; + // TODO: this return had to be commented out otherwise no file would be linted, figure out why. + // return undefined; } // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. diff --git a/packages/angular_devkit/build_webpack/src/tslint/schema.json b/packages/angular_devkit/build_angular/src/tslint/schema.json similarity index 89% rename from packages/angular_devkit/build_webpack/src/tslint/schema.json rename to packages/angular_devkit/build_angular/src/tslint/schema.json index 6df6e96dc6..33f4b03e60 100644 --- a/packages/angular_devkit/build_webpack/src/tslint/schema.json +++ b/packages/angular_devkit/build_angular/src/tslint/schema.json @@ -8,8 +8,16 @@ "description": "The name of the TSLint configuration file." }, "tsConfig": { - "type": "string", - "description": "The name of the TypeScript configuration file." + "description": "The name of the TypeScript configuration file.", + "oneOf": [ + { "type": "string" }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, "fix": { "type": "boolean", @@ -65,7 +73,5 @@ } }, "additionalProperties": false, - "required": [ - "tslintConfig" - ] -} + "required": [] +} \ No newline at end of file diff --git a/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts b/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts new file mode 100644 index 0000000000..f776160f03 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/add-file-replacements.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { BaseException, Path, join, normalize, virtualFs } from '@angular-devkit/core'; +import { Observable, forkJoin, of } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { + CurrentFileReplacement, + DeprecatedFileReplacment, + FileReplacement, +} from '../browser/schema'; + + +export class MissingFileReplacementException extends BaseException { + constructor(path: String) { + super(`The ${path} path in file replacements does not exist.`); + } +} + +// Note: This method changes the file replacements in place. +export function addFileReplacements( + root: Path, + host: virtualFs.AliasHost, + fileReplacements: FileReplacement[], +): Observable { + + if (fileReplacements.length === 0) { + return of(null); + } + + // Normalize the legacy format into the current one. + for (const fileReplacement of fileReplacements) { + const currentFormat = fileReplacement as CurrentFileReplacement; + const maybeOldFormat = fileReplacement as DeprecatedFileReplacment; + + if (maybeOldFormat.src && maybeOldFormat.replaceWith) { + currentFormat.replace = maybeOldFormat.src; + currentFormat.with = maybeOldFormat.replaceWith; + } + } + + const normalizedFileReplacements = fileReplacements as CurrentFileReplacement[]; + + // Ensure all the replacements exist. + const errorOnFalse = (path: string) => tap((exists: boolean) => { + if (!exists) { + throw new MissingFileReplacementException(path); + } + }); + + const existObservables = normalizedFileReplacements + .map(replacement => [ + host.exists(join(root, replacement.replace)).pipe(errorOnFalse(replacement.replace)), + host.exists(join(root, replacement.with)).pipe(errorOnFalse(replacement.with)), + ]) + .reduce((prev, curr) => prev.concat(curr), []); + + return forkJoin(existObservables).pipe( + tap(() => { + normalizedFileReplacements.forEach(replacement => { + host.aliases.set( + join(root, normalize(replacement.replace)), + join(root, normalize(replacement.with)), + ); + }); + }), + map(() => null), + ); +} diff --git a/packages/angular_devkit/build_webpack/src/utils/index.ts b/packages/angular_devkit/build_angular/src/utils/index.ts similarity index 86% rename from packages/angular_devkit/build_webpack/src/utils/index.ts rename to packages/angular_devkit/build_angular/src/utils/index.ts index ef85078838..9e6614690f 100644 --- a/packages/angular_devkit/build_webpack/src/utils/index.ts +++ b/packages/angular_devkit/build_angular/src/utils/index.ts @@ -7,3 +7,4 @@ */ export * from './run-module-as-observable-fork'; +export * from './add-file-replacements'; diff --git a/packages/angular_devkit/build_webpack/src/utils/run-module-as-observable-fork.ts b/packages/angular_devkit/build_angular/src/utils/run-module-as-observable-fork.ts similarity index 98% rename from packages/angular_devkit/build_webpack/src/utils/run-module-as-observable-fork.ts rename to packages/angular_devkit/build_angular/src/utils/run-module-as-observable-fork.ts index c73ff048d4..7ae29d28e6 100644 --- a/packages/angular_devkit/build_webpack/src/utils/run-module-as-observable-fork.ts +++ b/packages/angular_devkit/build_angular/src/utils/run-module-as-observable-fork.ts @@ -8,7 +8,7 @@ import { BuildEvent } from '@angular-devkit/architect'; import { ForkOptions, fork } from 'child_process'; import { resolve } from 'path'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; const treeKill = require('tree-kill'); diff --git a/packages/angular_devkit/build_webpack/src/utils/run-module-worker.js b/packages/angular_devkit/build_angular/src/utils/run-module-worker.js similarity index 100% rename from packages/angular_devkit/build_webpack/src/utils/run-module-worker.js rename to packages/angular_devkit/build_angular/src/utils/run-module-worker.js diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts b/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts new file mode 100644 index 0000000000..461d1ce88d --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/webpack-file-system-host-adapter.ts @@ -0,0 +1,154 @@ +/** + * @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 { FileDoesNotExistException, JsonObject, normalize, virtualFs } from '@angular-devkit/core'; +import { Callback, InputFileSystem } from '@ngtools/webpack/src/webpack'; +import { Stats } from 'fs'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, switchMap } from 'rxjs/operators'; + + +export class WebpackFileSystemHostAdapter implements InputFileSystem { + protected _syncHost: virtualFs.SyncDelegateHost | null = null; + + constructor(protected _host: virtualFs.Host) {} + + private _doHostCall(o: Observable, callback: Callback) { + const token = Symbol(); + let value: T | typeof token = token; + let error = false; + + try { + o.subscribe({ + error(err) { + error = true; + callback(err); + }, + next(v) { + value = v; + }, + complete() { + if (value !== token) { + callback(null, value); + } else { + callback(new Error('Unknown error happened.')); + } + }, + }); + } catch (err) { + // In some occasions, the error handler above will be called, then an exception will be + // thrown (by design in observable constructors in RxJS 5). Don't call the callback + // twice. + if (!error) { + callback(err); + } + } + } + + stat(path: string, callback: Callback): void { + const p = normalize('/' + path); + const result = this._host.stat(p); + + if (result === null) { + const o = this._host.exists(p).pipe( + switchMap(exists => { + if (!exists) { + throw new FileDoesNotExistException(p); + } + + return this._host.isDirectory(p).pipe( + mergeMap(isDirectory => { + return (isDirectory ? of(0) : this._host.read(p).pipe( + map(content => content.byteLength), + )).pipe( + map(size => [isDirectory, size]), + ); + }), + ); + }), + map(([isDirectory, size]) => { + return { + isFile() { return !isDirectory; }, + isDirectory() { return isDirectory; }, + size, + atime: new Date(), + mtime: new Date(), + ctime: new Date(), + birthtime: new Date(), + }; + }), + ); + + this._doHostCall(o, callback); + } else { + this._doHostCall(result, callback); + } + } + + readdir(path: string, callback: Callback): void { + return this._doHostCall(this._host.list(normalize('/' + path)), callback); + } + + readFile(path: string, callback: Callback): void { + const o = this._host.read(normalize('/' + path)).pipe( + map(content => virtualFs.fileBufferToString(content)), + ); + + return this._doHostCall(o, callback); + } + + readJson(path: string, callback: Callback): void { + const o = this._host.read(normalize('/' + path)).pipe( + map(content => JSON.parse(virtualFs.fileBufferToString(content))), + ); + + return this._doHostCall(o, callback); + } + + readlink(path: string, callback: Callback): void { + const err: NodeJS.ErrnoException = new Error('Not a symlink.'); + err.code = 'EINVAL'; + callback(err); + } + + statSync(path: string): Stats { + if (!this._syncHost) { + this._syncHost = new virtualFs.SyncDelegateHost(this._host); + } + + const result = this._syncHost.stat(normalize('/' + path)); + if (result) { + return result; + } else { + return {} as Stats; + } + } + readdirSync(path: string): string[] { + if (!this._syncHost) { + this._syncHost = new virtualFs.SyncDelegateHost(this._host); + } + + return this._syncHost.list(normalize('/' + path)); + } + readFileSync(path: string): string { + if (!this._syncHost) { + this._syncHost = new virtualFs.SyncDelegateHost(this._host); + } + + return virtualFs.fileBufferToString(this._syncHost.read(normalize('/' + path))); + } + readJsonSync(path: string): string { + return JSON.parse(this.readFileSync(path)); + } + readlinkSync(path: string): string { + const err: NodeJS.ErrnoException = new Error('Not a symlink.'); + err.code = 'EINVAL'; + throw err; + } + + purge(_changes?: string[] | string): void {} +} diff --git a/packages/angular_devkit/build_angular/test/app-shell/app-shell_spec_large.ts b/packages/angular_devkit/build_angular/test/app-shell/app-shell_spec_large.ts new file mode 100644 index 0000000000..b179c54e08 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/app-shell/app-shell_spec_large.ts @@ -0,0 +1,50 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, host, runTargetSpec } from '../utils'; + + +describe('AppShell Builder', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works (basic)', done => { + host.replaceInFile('src/app/app.module.ts', / BrowserModule/, ` + BrowserModule.withServerTransition({ appId: 'some-app' }) + `); + host.writeMultipleFiles({ + 'src/app/app.server.module.ts': ` + import { NgModule } from '@angular/core'; + import { ServerModule } from '@angular/platform-server'; + + import { AppModule } from './app.module'; + import { AppComponent } from './app.component'; + + @NgModule({ + imports: [ + AppModule, + ServerModule, + ], + bootstrap: [AppComponent], + }) + export class AppServerModule {} + `, + }); + + runTargetSpec(host, { project: 'app', target: 'app-shell' }).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/index.html'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/Welcome to app!/); + }), + ).subscribe(undefined, done.fail, done); + + }, Timeout.Complex); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/allow-js_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/allow-js_spec_large.ts new file mode 100644 index 0000000000..b7852103e2 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/allow-js_spec_large.ts @@ -0,0 +1,43 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder allow js', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + 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);`, + }); + + // TODO: this test originally edited tsconfig to have `"allowJs": true` but works without it. + // Investigate. + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('works with aot', (done) => { + 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 = { aot: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts new file mode 100644 index 0000000000..856f519eb9 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/aot_spec_large.ts @@ -0,0 +1,32 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder AOT', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { aot: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/platformBrowser.*bootstrapModuleFactory.*AppModuleNgFactory/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts new file mode 100644 index 0000000000..5cd705258b --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts @@ -0,0 +1,71 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder assets', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const assets: { [path: string]: string } = { + './src/folder/.gitkeep': '', + './src/folder/folder-asset.txt': 'folder-asset.txt', + './src/glob-asset.txt': 'glob-asset.txt', + './src/output-asset.txt': 'output-asset.txt', + }; + const matches: { [path: string]: string } = { + './dist/folder/folder-asset.txt': 'folder-asset.txt', + './dist/glob-asset.txt': 'glob-asset.txt', + './dist/output-folder/output-asset.txt': 'output-asset.txt', + }; + host.writeMultipleFiles(assets); + + const overrides = { + assets: [ + { glob: 'glob-asset.txt', input: 'src/', output: '/' }, + { glob: 'output-asset.txt', input: 'src/', output: '/output-folder' }, + { glob: '**/*', input: 'src/folder', output: '/folder' }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + // Assets we expect should be there. + Object.keys(matches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(matches[fileName]); + }); + // .gitkeep should not be there. + expect(host.scopedSync().exists(normalize('./dist/folder/.gitkeep'))).toBe(false); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('fails with non-absolute output path', (done) => { + const assets: { [path: string]: string } = { + './node_modules/some-package/node_modules-asset.txt': 'node_modules-asset.txt', + }; + host.writeMultipleFiles(assets); + const overrides = { + assets: [{ + glob: '**/*', input: '../node_modules/some-package/', output: '../temp', + }], + }; + + runTargetSpec(host, browserTargetSpec, overrides).subscribe(undefined, done, done.fail); + + // The node_modules folder must be deleted, otherwise code that tries to find the + // node_modules folder will hit this one and can fail. + host.scopedSync().delete(normalize('./node_modules')); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/base-href_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/base-href_spec_large.ts new file mode 100644 index 0000000000..34f457108e --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/base-href_spec_large.ts @@ -0,0 +1,37 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder base href', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + 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 = { baseHref: '/myUrl' }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + expect(content).toMatch(//); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts new file mode 100644 index 0000000000..1a1d84d862 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts @@ -0,0 +1,31 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder build optimizer', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { aot: true, buildOptimizer: true }; + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).not.toMatch(/\.decorators =/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts new file mode 100644 index 0000000000..5169b0e7f7 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts @@ -0,0 +1,56 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { TestLogger, Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder bundle budgets', () => { + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('accepts valid bundles', (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'allScript', maximumError: '100mb' }], + }; + + const logger = new TestLogger('rebuild-type-errors'); + + runTargetSpec(host, browserTargetSpec, overrides, logger).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(logger.includes('WARNING')).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); + + it('shows errors', (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'all', maximumError: '100b' }], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); + + it('shows warnings', (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'all', minimumWarning: '100mb' }], + }; + + const logger = new TestLogger('rebuild-type-errors'); + + runTargetSpec(host, browserTargetSpec, overrides, logger).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(logger.includes('WARNING')).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/circular-dependency_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/circular-dependency_spec_large.ts new file mode 100644 index 0000000000..f5b85c90ff --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/circular-dependency_spec_large.ts @@ -0,0 +1,29 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { TestLogger, Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder circular dependency detection', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + host.appendToFile('src/app/app.component.ts', + `import { AppModule } from './app.module'; console.log(AppModule);`); + + const overrides = { baseHref: '/myUrl' }; + const logger = new TestLogger('circular-dependencies'); + + runTargetSpec(host, browserTargetSpec, overrides, logger).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(logger.includes('Circular dependency detected')).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/custom-lazy-modules_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/custom-lazy-modules_spec_large.ts new file mode 100644 index 0000000000..7e0b6647aa --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/custom-lazy-modules_spec_large.ts @@ -0,0 +1,57 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +// import { concatMap, tap, toArray } from 'rxjs/operators'; +// import { host, browserTargetSpec, makeWorkspace } from '../utils'; +// import { lazyModuleFiles, lazyModuleImport } from './rebuild_spec_large'; + + +// TODO: re-enable this test when the custom lazy module changes have been ported over to +// webpack-builder from the CLI. +// describe('Browser Builder custom lazy modules', () => { +// +// const architect = new Architect(normalize(workspaceRoot), host); +// const outputPath = normalize('dist'); + +// beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); +// afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + +// it('works', (done) => { +// host.writeMultipleFiles(lazyModuleFiles); +// host.writeMultipleFiles(lazyModuleImport); +// host.writeMultipleFiles({ +// 'src/app/app.component.ts': ` +// import { Component, SystemJsNgModuleLoader } from '@angular/core'; + +// @Component({ +// selector: 'app-root', +// templateUrl: './app.component.html', +// styleUrls: ['./app.component.css'], +// }) +// export class AppComponent { +// title = 'app'; +// constructor(loader: SystemJsNgModuleLoader) { +// // Module will be split at build time and loaded when requested below +// loader.load('app/lazy/lazy.module#LazyModule') +// .then((factory) => { /* Use factory here */ }); +// } +// }`, +// }); + +// const overrides = { lazyModules: ['app/lazy/lazy.module'] }; + +// architect.loadWorkspaceFromJson(makeWorkspace(browserTargetSpec)).pipe( +// concatMap(() => architect.run(architect.getTarget({ overrides }))), +// tap((buildEvent) => expect(buildEvent.success).toBe(true)), +// tap(() => +// expect(host.scopedSync().exists(join(outputPath, 'lazy.module.js'))).toBe(true)), +// ).subscribe(undefined, done.fail, done); +// }, 30000); +// }); diff --git a/packages/angular_devkit/build_angular/test/browser/deploy-url_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/deploy-url_spec_large.ts new file mode 100644 index 0000000000..060a0cfbac --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/deploy-url_spec_large.ts @@ -0,0 +1,54 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { concatMap, tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder deploy url', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('uses deploy url for bundles urls', (done) => { + const overrides = { deployUrl: 'deployUrl/' }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('deployUrl/main.js'); + }), + concatMap(() => runTargetSpec(host, browserTargetSpec, + { deployUrl: 'http://example.com/some/path/' })), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('http://example.com/some/path/main.js'); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('uses deploy url for in webpack runtime', (done) => { + const overrides = { deployUrl: 'deployUrl/' }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'runtime.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('deployUrl/'); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + +}); diff --git a/packages/angular_devkit/build_angular/test/browser/errors_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/errors_spec_large.ts new file mode 100644 index 0000000000..6a7941e29d --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/errors_spec_large.ts @@ -0,0 +1,56 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { TestLogger, Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder errors', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('shows error when files are not part of the compilation', (done) => { + host.replaceInFile('src/tsconfig.app.json', '"compilerOptions": {', ` + "files": ["main.ts"], + "compilerOptions": { + `); + const logger = new TestLogger('errors-compilation'); + + runTargetSpec(host, browserTargetSpec, undefined, logger).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(false); + expect(logger.includes('polyfills.ts is missing from the TypeScript')).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('shows TS syntax errors', (done) => { + host.appendToFile('src/app/app.component.ts', ']]]'); + const logger = new TestLogger('errors-syntax'); + + runTargetSpec(host, browserTargetSpec, undefined, logger).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(false); + expect(logger.includes('Declaration or statement expected.')).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('shows static analysis errors', (done) => { + host.replaceInFile('src/app/app.component.ts', `'app-root'`, `(() => 'app-root')()`); + const logger = new TestLogger('errors-static'); + + runTargetSpec(host, browserTargetSpec, { aot: true }, logger).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(false); + expect(logger.includes('Function expressions are not supported in')).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + +}); diff --git a/packages/angular_devkit/build_angular/test/browser/i18n_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/i18n_spec_large.ts new file mode 100644 index 0000000000..db1a61efe1 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/i18n_spec_large.ts @@ -0,0 +1,118 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder i18n', () => { + const outputPath = normalize('dist'); + const emptyTranslationFile = ` + + + + + + + `; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('uses translations', (done) => { + host.writeMultipleFiles({ + 'src/locale/messages.fr.xlf': ` + + + + + + Hello i18n! + Bonjour i18n! + An introduction header for this sample + + + + + `, + }); + + host.appendToFile('src/app/app.component.html', + '

Hello i18n!

'); + + const overrides = { + aot: true, + i18nFile: 'src/locale/messages.fr.xlf', + i18nFormat: 'true', + i18nLocale: 'fr', + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/Bonjour i18n!/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('ignores missing translations', (done) => { + const overrides = { + aot: true, + i18nFile: 'src/locale/messages.fr.xlf', + i18nFormat: 'true', + i18nLocale: 'fr', + i18nMissingTranslation: 'ignore', + }; + + host.writeMultipleFiles({ 'src/locale/messages.fr.xlf': emptyTranslationFile }); + host.appendToFile('src/app/app.component.html', '

Other content

'); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/Other content/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('reports errors for missing translations', (done) => { + const overrides = { + aot: true, + i18nFile: 'src/locale/messages.fr.xlf', + i18nFormat: 'true', + i18nLocale: 'fr', + i18nMissingTranslation: 'error', + }; + + host.writeMultipleFiles({ 'src/locale/messages.fr.xlf': emptyTranslationFile }); + host.appendToFile('src/app/app.component.html', '

Other content

'); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('register locales', (done) => { + const overrides = { aot: true, i18nLocale: 'fr_FR' }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/registerLocaleData/); + expect(content).toMatch(/angular_common_locales_fr/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/index-bom_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/index-bom_spec_large.ts new file mode 100644 index 0000000000..6164ce1c15 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/index-bom_spec_large.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder works with BOM index.html', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works with UTF-8 BOM', (done) => { + host.writeMultipleFiles({ + 'src/index.html': Buffer.from( + '\ufeff', + 'utf8'), + }); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + // tslint:disable-next-line:max-line-length + expect(content).toBe(``); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('works with UTF16 LE BOM', (done) => { + host.writeMultipleFiles({ + 'src/index.html': Buffer.from( + '\ufeff', + 'utf16le'), + }); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + // tslint:disable-next-line:max-line-length + expect(content).toBe(``); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/lazy-module_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/lazy-module_spec_large.ts new file mode 100644 index 0000000000..0cbff858b1 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/lazy-module_spec_large.ts @@ -0,0 +1,214 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { BrowserBuilderSchema } from '../../src'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +export const lazyModuleFiles: { [path: string]: string } = { + 'src/app/lazy/lazy-routing.module.ts': ` + import { NgModule } from '@angular/core'; + import { Routes, RouterModule } from '@angular/router'; + + const routes: Routes = []; + + @NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] + }) + export class LazyRoutingModule { } + `, + 'src/app/lazy/lazy.module.ts': ` + import { NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + + import { LazyRoutingModule } from './lazy-routing.module'; + + @NgModule({ + imports: [ + CommonModule, + LazyRoutingModule + ], + declarations: [] + }) + export class LazyModule { } + `, +}; + +export const lazyModuleImport: { [path: string]: string } = { + 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { HttpModule } from '@angular/http'; + + import { AppComponent } from './app.component'; + import { RouterModule } from '@angular/router'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + HttpModule, + RouterModule.forRoot([ + { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule' } + ]) + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, +}; + +describe('Browser Builder lazy modules', () => { + + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('supports lazy bundle for lazy routes', (done) => { + host.writeMultipleFiles(lazyModuleFiles); + host.writeMultipleFiles(lazyModuleImport); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'lazy-lazy-module.js'))).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports lazy bundle for import() calls`, (done) => { + host.writeMultipleFiles({ + 'src/lazy-module.ts': 'export const value = 42;', + 'src/main.ts': `import('./lazy-module');`, + }); + // Using `import()` in TS require targetting `esnext` modules. + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '0.js'))).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports lazy bundle for dynamic import() calls`, (done) => { + host.writeMultipleFiles({ + 'src/lazy-module.ts': 'export const value = 42;', + 'src/main.ts': ` + const lazyFileName = 'module'; + import(/*webpackChunkName: '[request]'*/'./lazy-' + lazyFileName); + `, + }); + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, 'lazy-module.js'))).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports lazy bundle for System.import() calls`, (done) => { + host.writeMultipleFiles({ + 'src/lazy-module.ts': 'export const value = 42;', + 'src/main.ts': `declare var System: any; System.import('./lazy-module');`, + }); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '0.js'))).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports hiding lazy bundle module name`, (done) => { + host.writeMultipleFiles({ + 'src/lazy-module.ts': 'export const value = 42;', + 'src/main.ts': `const lazyFileName = 'module'; import('./lazy-' + lazyFileName);`, + }); + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + const overrides: Partial = { namedChunks: false }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '0.js'))).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports making a common bundle for shared lazy modules`, (done) => { + host.writeMultipleFiles({ + 'src/one.ts': `import * as http from '@angular/http'; console.log(http);`, + 'src/two.ts': `import * as http from '@angular/http'; console.log(http);`, + 'src/main.ts': `import('./one'); import('./two');`, + }); + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '0.js'))).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '1.js'))).toBe(true)), + // TODO: the chunk with common modules used to be called `common`, see why that changed. + tap(() => expect(host.scopedSync().exists(join(outputPath, '2.js'))).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports disabling the common bundle`, (done) => { + host.writeMultipleFiles({ + 'src/one.ts': `import * as http from '@angular/http'; console.log(http);`, + 'src/two.ts': `import * as http from '@angular/http'; console.log(http);`, + 'src/main.ts': `import('./one'); import('./two');`, + }); + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + const overrides: Partial = { commonChunk: false }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '0.js'))).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '1.js'))).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, '2.js'))).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports extra lazy modules array`, (done) => { + host.writeMultipleFiles(lazyModuleFiles); + host.writeMultipleFiles(lazyModuleImport); + host.writeMultipleFiles({ + 'src/app/app.component.ts': ` + import { Component, SystemJsNgModuleLoader } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], + }) + export class AppComponent { + title = 'app'; + constructor(loader: SystemJsNgModuleLoader) { + // Module will be split at build time and loaded when requested below + loader.load('app/lazy/lazy.module#LazyModule') + .then((factory) => { /* Use factory here */ }); + } + }`, + }); + host.replaceInFile('src/tsconfig.app.json', `"module": "es2015"`, `"module": "esnext"`); + + const overrides: Partial = { lazyModules: ['src/app/lazy/lazy.module'] }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => expect(host.scopedSync().exists(join(outputPath, 'lazy-lazy-module.js'))) + .toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/license-extraction_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/license-extraction_spec_large.ts new file mode 100644 index 0000000000..11edc1621c --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/license-extraction_spec_large.ts @@ -0,0 +1,34 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder license extraction', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + // Ignored because license works when trying manually on a project, but doesn't work here. + // TODO: fix VFS use in webpack and the test host, and reenable this test. + xit('works', (done) => { + // TODO: make license extraction independent from optimization level. + const overrides = { extractLicenses: true, optimization: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, '3rdpartylicenses.txt'); + expect(host.scopedSync().exists(fileName)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/no-entry-module_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/no-entry-module_spec_large.ts new file mode 100644 index 0000000000..8eb0fce414 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/no-entry-module_spec_large.ts @@ -0,0 +1,28 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder no entry module', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + // Remove the bootstrap but keep a reference to AppModule so the import is not elided. + host.replaceInFile('src/main.ts', /platformBrowserDynamic.*?bootstrapModule.*?;/, ''); + host.appendToFile('src/main.ts', 'console.log(AppModule);'); + + const overrides = { baseHref: '/myUrl' }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts new file mode 100644 index 0000000000..e9c8c9502c --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/optimization-level_spec_large.ts @@ -0,0 +1,48 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder optimization level', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { optimization: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + // Bundle contents should be uglified, which includes variable mangling. + expect(content).not.toContain('AppComponent'); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); + + it('tsconfig target changes optimizations to use ES2015', (done) => { + host.replaceInFile('tsconfig.json', '"target": "es5"', '"target": "es2015"'); + + const overrides = { optimization: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'vendor.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + expect(content).toMatch(/class \w{constructor\(\){/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/output-hashing_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/output-hashing_spec_large.ts new file mode 100644 index 0000000000..ae577e8aeb --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/output-hashing_spec_large.ts @@ -0,0 +1,163 @@ +/** + * @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 { normalize } from '@angular-devkit/core'; +import { concatMap, tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; +import { lazyModuleFiles, lazyModuleImport } from './lazy-module_spec_large'; + + +describe('Browser Builder output hashing', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('updates hash as content changes', (done) => { + 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(lazyModuleImport); + + 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. + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap(() => { + // Save the current hashes. + oldHashes = generateFileHashMap(); + host.writeMultipleFiles(lazyModuleFiles); + host.writeMultipleFiles(lazyModuleImport); + }), + // Lazy chunk hash should have changed without modifying main bundle. + concatMap(() => runTargetSpec(host, browserTargetSpec, overrides)), + tap(() => { + newHashes = generateFileHashMap(); + validateHashes(oldHashes, newHashes, []); + oldHashes = newHashes; + host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' }); + }), + // Style hash should change. + concatMap(() => runTargetSpec(host, browserTargetSpec, overrides)), + tap(() => { + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, overrides)), + tap(() => { + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, overrides)), + tap(() => { + newHashes = generateFileHashMap(); + validateHashes(oldHashes, newHashes, ['lazy.module']); + oldHashes = newHashes; + host.appendToFile('src/main.ts', ''); + }), + // Nothing should have changed. + concatMap(() => runTargetSpec(host, browserTargetSpec, overrides)), + tap(() => { + newHashes = generateFileHashMap(); + validateHashes(oldHashes, newHashes, []); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Massive); + + it('supports options', (done) => { + host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` }); + host.writeMultipleFiles(lazyModuleFiles); + host.writeMultipleFiles(lazyModuleImport); + + // 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. + runTargetSpec(host, browserTargetSpec, { outputHashing: 'all', extractCss: true }).pipe( + tap(() => { + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { outputHashing: 'none', extractCss: true })), + tap(() => { + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { outputHashing: 'media', extractCss: true })), + tap(() => { + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { outputHashing: 'bundles', extractCss: true })), + tap(() => { + 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(); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/output-path_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/output-path_spec_large.ts new file mode 100644 index 0000000000..703f3f646e --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/output-path_spec_large.ts @@ -0,0 +1,42 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec, workspaceRoot } from '../utils'; + + +describe('Browser Builder output path', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('deletes output path', (done) => { + // Write a file to the output path to later verify it was deleted. + host.scopedSync().write(join(outputPath, 'file.txt'), virtualFs.stringToFileBuffer('file')); + // Delete an app file to force a failed compilation. + // Failed compilations still delete files, but don't output any. + host.delete(join(workspaceRoot, 'src', 'app', 'app.component.ts')).subscribe({ + error: done.fail, + }); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(false); + expect(host.scopedSync().exists(outputPath)).toBe(false); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('does not allow output path to be project root', (done) => { + const overrides = { outputPath: './' }; + + runTargetSpec(host, browserTargetSpec, overrides).subscribe(undefined, done, done.fail); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/poll_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/poll_spec_large.ts new file mode 100644 index 0000000000..61b524c937 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/poll_spec_large.ts @@ -0,0 +1,42 @@ +/** + * @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 { debounceTime, take, tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder poll', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { watch: true, poll: 1000 }; + let msAvg = 1000; + let lastTime: number; + runTargetSpec(host, browserTargetSpec, overrides).pipe( + // Debounce 1s, otherwise changes are too close together and polling doesn't work. + debounceTime(1000), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const currTime = Date.now(); + if (lastTime) { + const ms = Math.floor((currTime - lastTime)); + msAvg = (msAvg + ms) / 2; + } + lastTime = currTime; + host.appendToFile('src/main.ts', 'console.log(1);'); + }), + take(5), + ).subscribe(undefined, done.fail, () => { + // Check if the average is between 1750 and 2750, allowing for a 1000ms variance. + expect(msAvg).toBeGreaterThan(1750); + expect(msAvg).toBeLessThan(2750); + done(); + }); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/rebuild_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/rebuild_spec_large.ts new file mode 100644 index 0000000000..b4ab007b50 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/rebuild_spec_large.ts @@ -0,0 +1,350 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { debounceTime, take, tap } from 'rxjs/operators'; +import { TestLogger, Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; +import { lazyModuleFiles, lazyModuleImport } from './lazy-module_spec_large'; + + +// TODO: replace this with an "it()" macro that's reusable globally. +let linuxOnlyIt: typeof it = it; +if (process.platform.startsWith('win')) { + linuxOnlyIt = xit; +} + + +describe('Browser Builder rebuilds', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + + it('rebuilds on TS file changes', (done) => { + if (process.env['APPVEYOR']) { + // TODO: This test fails on Windows CI, figure out why. + done(); + + return; + } + const goldenValueFiles: { [path: string]: string } = { + 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + + console.log('$$_E2E_GOLDEN_VALUE_1'); + export let X = '$$_E2E_GOLDEN_VALUE_2'; + `, + 'src/main.ts': ` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + + import * as m from './app/app.module'; + console.log(m.X); + console.log('$$_E2E_GOLDEN_VALUE_3'); + `, + }; + + const overrides = { watch: true }; + + let buildNumber = 0; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + // We must debounce on watch mode because file watchers are not very accurate. + // Changes from just before a process runs can be picked up and cause rebuilds. + // In this case, cleanup from the test right before this one causes a few rebuilds. + debounceTime(1000), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + buildNumber += 1; + switch (buildNumber) { + case 1: + // No lazy chunk should exist. + expect(host.scopedSync().exists(join(outputPath, 'lazy-module.js'))).toBe(false); + // Write the lazy chunk files. Order matters when writing these, because of imports. + host.writeMultipleFiles(lazyModuleFiles); + host.writeMultipleFiles(lazyModuleImport); + break; + + case 2: + // A lazy chunk should have been with the filename. + expect(host.scopedSync().exists(join(outputPath, 'lazy-lazy-module.js'))).toBe(true); + host.writeMultipleFiles(goldenValueFiles); + break; + + case 3: + // The golden values should be present and in the right order. + const re = new RegExp( + /\$\$_E2E_GOLDEN_VALUE_1(.|\n|\r)*/.source + + /\$\$_E2E_GOLDEN_VALUE_2(.|\n|\r)*/.source + + /\$\$_E2E_GOLDEN_VALUE_3/.source, + ); + const fileName = './dist/main.js'; + const content = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(fileName)), + ); + expect(content).toMatch(re); + break; + + default: + break; + } + }), + take(3), + ).subscribe(undefined, done.fail, done); + }, Timeout.Massive); + + it('rebuilds on CSS changes', (done) => { + const overrides = { watch: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + debounceTime(500), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => host.appendToFile('src/app/app.component.css', ':host { color: blue; }')), + take(2), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); + + it('type checks on rebuilds', (done) => { + host.writeMultipleFiles({ + 'src/funky2.ts': `export const funky2 = (value: string) => value + 'hello';`, + 'src/funky.ts': `export * from './funky2';`, + }); + host.appendToFile('src/main.ts', ` + import { funky2 } from './funky'; + console.log(funky2('town')); + `); + + const overrides = { watch: true, forkTypeChecker: false }; + const logger = new TestLogger('rebuild-type-errors'); + const typeError = `is not assignable to parameter of type 'number'`; + let buildNumber = 0; + + runTargetSpec(host, browserTargetSpec, overrides, logger).pipe( + debounceTime(1000), + tap((buildEvent) => { + buildNumber += 1; + switch (buildNumber) { + case 1: + expect(buildEvent.success).toBe(true); + // Make an invalid version of the file. + // Should trigger a rebuild, this time an error is expected. + host.writeMultipleFiles({ + 'src/funky2.ts': `export const funky2 = (value: number) => value + 1;`, + }); + break; + + case 2: + // The second build should error out with a type error on the type of an argument. + expect(buildEvent.success).toBe(false); + expect(logger.includes(typeError)).toBe(true); + logger.clear(); + // Change an UNRELATED file and the error should still happen. + // Should trigger a rebuild, this time an error is also expected. + host.appendToFile('src/app/app.module.ts', `console.log(1);`); + break; + + case 3: + // The third build should have the same error as the first. + expect(buildEvent.success).toBe(false); + expect(logger.includes(typeError)).toBe(true); + logger.clear(); + // Fix the error. + host.writeMultipleFiles({ + 'src/funky2.ts': `export const funky2 = (value: string) => value + 'hello';`, + }); + break; + + default: + expect(buildEvent.success).toBe(true); + break; + } + }), + take(4), + ).subscribe(undefined, done.fail, done); + }, Timeout.Massive); + + it('rebuilds on type changes', (done) => { + host.writeMultipleFiles({ 'src/type.ts': `export type MyType = number;` }); + host.appendToFile('src/main.ts', `import { MyType } from './type';`); + + const overrides = { watch: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + debounceTime(1000), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => host.writeMultipleFiles({ 'src/type.ts': `export type MyType = string;` })), + take(2), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + + linuxOnlyIt('rebuilds after errors in AOT', (done) => { + // Save the original contents of `./src/app/app.component.ts`. + const origContent = virtualFs.fileBufferToString( + host.scopedSync().read(normalize('src/app/app.component.ts'))); + // Add a major static analysis error on a non-main file to the initial build. + host.replaceInFile('./src/app/app.component.ts', `'app-root'`, `(() => 'app-root')()`); + + const overrides = { watch: true, aot: true, forkTypeChecker: false }; + const logger = new TestLogger('rebuild-aot-errors'); + const staticAnalysisError = 'Function expressions are not supported in decorators'; + const syntaxError = 'Declaration or statement expected.'; + let buildNumber = 0; + + runTargetSpec(host, browserTargetSpec, overrides, logger).pipe( + debounceTime(1000), + tap((buildEvent) => { + buildNumber += 1; + switch (buildNumber) { + case 1: + // The first build should error out with a static analysis error. + expect(buildEvent.success).toBe(false); + expect(logger.includes(staticAnalysisError)).toBe(true); + logger.clear(); + // Fix the static analysis error. + host.writeMultipleFiles({ 'src/app/app.component.ts': origContent }); + break; + + case 2: + expect(buildEvent.success).toBe(true); + // Add an syntax error to a non-main file. + host.appendToFile('src/app/app.component.ts', `]]]`); + break; + + case 3: + // The third build should have TS syntax error. + expect(buildEvent.success).toBe(false); + expect(logger.includes(syntaxError)).toBe(true); + logger.clear(); + // Fix the syntax error, but add the static analysis error again. + host.writeMultipleFiles({ + 'src/app/app.component.ts': origContent.replace(`'app-root'`, `(() => 'app-root')()`), + }); + break; + + case 4: + expect(buildEvent.success).toBe(false); + // Restore the file to a error-less state. + host.writeMultipleFiles({ 'src/app/app.component.ts': origContent }); + break; + + case 5: + // The fifth build should have everything fixed.. + expect(buildEvent.success).toBe(true); + expect(logger.includes(staticAnalysisError)).toBe(true); + break; + } + }), + take(5), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); + + + linuxOnlyIt('rebuilds AOT factories', (done) => { + + host.writeMultipleFiles({ + 'src/app/app.component.css': ` + @import './imported-styles.css'; + body {background-color: #00f;} + `, + 'src/app/imported-styles.css': 'p {color: #f00;}', + }); + + const overrides = { watch: true, aot: true, forkTypeChecker: false }; + let buildNumber = 0; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + debounceTime(1000), + tap((buildEvent) => { + buildNumber += 1; + const fileName = './dist/main.js'; + let content; + switch (buildNumber) { + case 1: + // Trigger a few rebuilds first. + // The AOT compiler is still optimizing rebuilds on the first rebuilds. + expect(buildEvent.success).toBe(true); + host.appendToFile('src/main.ts', 'console.log(1);'); + break; + + case 2: + expect(buildEvent.success).toBe(true); + host.appendToFile('src/main.ts', 'console.log(1);'); + break; + + case 3: + // Change the component html. + expect(buildEvent.success).toBe(true); + host.appendToFile('src/app/app.component.html', '

HTML_REBUILD_STRING

'); + break; + + case 4: + // Check if html changes are added to factories. + expect(buildEvent.success).toBe(true); + content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('HTML_REBUILD_STRING'); + // Change the component css. + host.appendToFile('src/app/app.component.css', 'CSS_REBUILD_STRING {color: #f00;}'); + break; + + case 5: + // Check if css changes are added to factories. + expect(buildEvent.success).toBe(true); + content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('CSS_REBUILD_STRING'); + // Change the component css import. + host.appendToFile('src/app/app.component.css', 'CSS_DEP_REBUILD_STRING {color: #f00;}'); + break; + + case 6: + // Check if css import changes are added to factories. + expect(buildEvent.success).toBe(true); + content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('CSS_DEP_REBUILD_STRING'); + // Change the component itself. + host.replaceInFile('src/app/app.component.ts', 'app-root', + 'app-root-FACTORY_REBUILD_STRING'); + break; + + case 7: + // Check if component changes are added to factories. + expect(buildEvent.success).toBe(true); + content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain('FACTORY_REBUILD_STRING'); + break; + } + }), + take(7), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts new file mode 100644 index 0000000000..9937caba86 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/replacements_spec_large.ts @@ -0,0 +1,100 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder file replacements', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + beforeEach(() => host.writeMultipleFiles({ + 'src/meaning-too.ts': 'export var meaning = 42;', + 'src/meaning.ts': `export var meaning = 10;`, + + 'src/main.ts': ` + import { meaning } from './meaning'; + + console.log(meaning); + `, + })); + + it('allows file replacements', (done) => { + const overrides = { + fileReplacements: [ + { + replace: normalize('/src/meaning.ts'), + with: normalize('/src/meaning-too.ts'), + }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + expect(virtualFs.fileBufferToString(host.scopedSync().read(fileName))) + .toMatch(/meaning\s*=\s*42/); + expect(virtualFs.fileBufferToString(host.scopedSync().read(fileName))) + .not.toMatch(/meaning\s*=\s*10/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`allows file replacements with deprecated format`, (done) => { + const overrides = { + fileReplacements: [ + { + src: normalize('/src/meaning.ts'), + replaceWith: normalize('/src/meaning-too.ts'), + }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js'); + expect(virtualFs.fileBufferToString(host.scopedSync().read(fileName))) + .toMatch(/meaning\s*=\s*42/); + expect(virtualFs.fileBufferToString(host.scopedSync().read(fileName))) + .not.toMatch(/meaning\s*=\s*10/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`fails compilation with missing 'replace' file`, (done) => { + const overrides = { + fileReplacements: [ + { + replace: normalize('/src/meaning.ts'), + with: normalize('/src/meaning-three.ts'), + }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).subscribe(undefined, done, done.fail); + }, Timeout.Basic); + + it(`fails compilation with missing 'with' file`, (done) => { + const overrides = { + fileReplacements: [ + { + replace: normalize('/src/meaning-three.ts'), + with: normalize('/src/meaning-too.ts'), + }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).subscribe(undefined, done, done.fail); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/scripts-array_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/scripts-array_spec_large.ts new file mode 100644 index 0000000000..391b260bae --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/scripts-array_spec_large.ts @@ -0,0 +1,135 @@ +/** + * @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 { PathFragment, join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder scripts array', () => { + + const outputPath = normalize('dist'); + const scripts: { [path: string]: string } = { + 'src/input-script.js': 'console.log(\'input-script\'); var number = 1+1;', + 'src/zinput-script.js': 'console.log(\'zinput-script\');', + 'src/finput-script.js': 'console.log(\'finput-script\');', + 'src/uinput-script.js': 'console.log(\'uinput-script\');', + 'src/binput-script.js': 'console.log(\'binput-script\');', + 'src/ainput-script.js': 'console.log(\'ainput-script\');', + 'src/cinput-script.js': 'console.log(\'cinput-script\');', + 'src/lazy-script.js': 'console.log(\'lazy-script\');', + 'src/pre-rename-script.js': 'console.log(\'pre-rename-script\');', + 'src/pre-rename-lazy-script.js': 'console.log(\'pre-rename-lazy-script\');', + }; + const getScriptsOption = () => [ + 'src/input-script.js', + 'src/zinput-script.js', + 'src/finput-script.js', + 'src/uinput-script.js', + 'src/binput-script.js', + 'src/ainput-script.js', + 'src/cinput-script.js', + { input: 'src/lazy-script.js', bundleName: 'lazy-script', lazy: true }, + { input: 'src/pre-rename-script.js', bundleName: 'renamed-script' }, + { input: 'src/pre-rename-lazy-script.js', bundleName: 'renamed-lazy-script', lazy: true }, + ]; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const matches: { [path: string]: string } = { + './dist/scripts.js': 'input-script', + './dist/lazy-script.js': 'lazy-script', + './dist/renamed-script.js': 'pre-rename-script', + './dist/renamed-lazy-script.js': 'pre-rename-lazy-script', + './dist/main.js': 'input-script', + './dist/index.html': '' + + '' + + '' + + '' + + '' + + '', + }; + + host.writeMultipleFiles(scripts); + host.appendToFile('src/main.ts', '\nimport \'./input-script.js\';'); + + // Remove styles so we don't have to account for them in the index.html order check. + const overrides = { + styles: [], + scripts: getScriptsOption(), + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => Object.keys(matches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(matches[fileName]); + })), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('uglifies, uses sourcemaps, and adds hashes', (done) => { + host.writeMultipleFiles(scripts); + + const overrides = { + optimization: true, + sourceMap: true, + outputHashing: 'all', + scripts: getScriptsOption(), + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const scriptsBundle = host.fileMatchExists(outputPath, /scripts\.[0-9a-f]{20}\.js/); + expect(scriptsBundle).toBeTruthy(); + const fileName = join(outputPath, scriptsBundle as PathFragment); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch('var number=2;'); + expect(host.fileMatchExists(outputPath, /scripts\.[0-9a-f]{20}\.js\.map/)) + .toBeTruthy(); + expect(host.fileMatchExists(outputPath, /renamed-script\.[0-9a-f]{20}\.js/)) + .toBeTruthy(); + expect(host.fileMatchExists(outputPath, /renamed-script\.[0-9a-f]{20}\.js\.map/)) + .toBeTruthy(); + expect(host.fileMatchExists(outputPath, /scripts\.[0-9a-f]{20}\.js/)).toBeTruthy(); + expect(host.scopedSync().exists(normalize('dist/lazy-script.js'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/lazy-script.js.map'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/renamed-lazy-script.js'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/renamed-lazy-script.js.map'))) + .toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Complex); + + it('preserves script order', (done) => { + host.writeMultipleFiles(scripts); + + const overrides = { scripts: getScriptsOption() }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const re = new RegExp( + /.*['"]input-script['"](.|\n|\r)*/.source + + /['"]zinput-script['"](.|\n|\r)*/.source + + /['"]finput-script['"](.|\n|\r)*/.source + + /['"]uinput-script['"](.|\n|\r)*/.source + + /['"]binput-script['"](.|\n|\r)*/.source + + /['"]ainput-script['"](.|\n|\r)*/.source + + /['"]cinput-script['"]/.source, + ); + const fileName = './dist/scripts.js'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(re); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts new file mode 100644 index 0000000000..c65292c6b5 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts @@ -0,0 +1,83 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder', () => { + const manifest = { + index: '/index.html', + assetGroups: [ + { + name: 'app', + installMode: 'prefetch', + resources: { + files: [ '/favicon.ico', '/index.html' ], + versionedFiles: [ + '/*.bundle.css', + '/*.bundle.js', + '/*.chunk.js', + ], + }, + }, + { + name: 'assets', + installMode: 'lazy', + updateMode: 'prefetch', + resources: { + files: [ '/assets/**' ], + }, + }, + ], + }; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('errors if no ngsw-config.json is present', (done) => { + const overrides = { serviceWorker: true }; + + runTargetSpec(host, browserTargetSpec, overrides) + .subscribe(event => { + expect(event.success).toBe(false); + }, done, done.fail); + }, Timeout.Basic); + + it('works with service worker', (done) => { + host.writeMultipleFiles({ + 'src/ngsw-config.json': JSON.stringify(manifest), + }); + + const overrides = { serviceWorker: true }; + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap(buildEvent => { + expect(buildEvent.success).toBe(true); + expect(host.scopedSync().exists(normalize('dist/ngsw.json'))); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('works with service worker and baseHref', (done) => { + host.writeMultipleFiles({ + 'src/ngsw-config.json': JSON.stringify(manifest), + }); + + const overrides = { serviceWorker: true, baseHref: '/foo/bar' }; + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap(buildEvent => { + expect(buildEvent.success).toBe(true); + expect(host.scopedSync().exists(normalize('dist/ngsw.json'))); + expect(virtualFs.fileBufferToString( + host.scopedSync().read(normalize('dist/ngsw.json')), + )).toMatch(/"\/foo\/bar\/index.html"/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts new file mode 100644 index 0000000000..305f6bb0a9 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/source-map_spec_large.ts @@ -0,0 +1,57 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder source map', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { sourceMap: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js.map'); + expect(host.scopedSync().exists(fileName)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('does not output source map when disabled', (done) => { + const overrides = { sourceMap: false }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'main.js.map'); + expect(host.scopedSync().exists(fileName)).toBe(false); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('supports eval source map', (done) => { + const overrides = { sourceMap: true, evalSourceMap: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(join(outputPath, 'main.js.map'))).toBe(false); + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + expect(content).toContain('eval("function webpackEmptyAsyncContext'); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/stats-json_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/stats-json_spec_large.ts new file mode 100644 index 0000000000..f881eeb37e --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/stats-json_spec_large.ts @@ -0,0 +1,31 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder stats json', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { statsJson: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'stats.json'); + expect(host.scopedSync().exists(fileName)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts new file mode 100644 index 0000000000..99ed697eab --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts @@ -0,0 +1,530 @@ +/** + * @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 { normalize, tags, virtualFs } from '@angular-devkit/core'; +import { concatMap, tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder styles', () => { + const extensionsWithImportSupport = ['css', 'scss', 'less', 'styl']; + const extensionsWithVariableSupport = ['scss', 'less', 'styl']; + const imgSvg = ` + + + + `; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('supports global styles', (done) => { + const styles: { [path: string]: string } = { + 'src/string-style.css': '.string-style { color: red }', + 'src/input-style.css': '.input-style { color: red }', + 'src/lazy-style.css': '.lazy-style { color: red }', + 'src/pre-rename-style.css': '.pre-rename-style { color: red }', + 'src/pre-rename-lazy-style.css': '.pre-rename-lazy-style { color: red }', + }; + const getStylesOption = () => [ + 'src/input-style.css', + { input: 'src/lazy-style.css', bundleName: 'lazy-style', lazy: true }, + { input: 'src/pre-rename-style.css', bundleName: 'renamed-style' }, + { input: 'src/pre-rename-lazy-style.css', bundleName: 'renamed-lazy-style', lazy: true }, + ]; + const cssMatches: { [path: string]: string } = { + './dist/styles.css': '.input-style', + './dist/lazy-style.css': '.lazy-style', + './dist/renamed-style.css': '.pre-rename-style', + './dist/renamed-lazy-style.css': '.pre-rename-lazy-style', + }; + const cssIndexMatches: { [path: string]: string } = { + './dist/index.html': '' + + '', + }; + const jsMatches: { [path: string]: string } = { + './dist/styles.js': '.input-style', + './dist/lazy-style.js': '.lazy-style', + './dist/renamed-style.js': '.pre-rename-style', + './dist/renamed-lazy-style.js': '.pre-rename-lazy-style', + }; + const jsIndexMatches: { [path: string]: string } = { + './dist/index.html': '' + + '' + + '' + + '' + + '' + + '', + }; + + host.writeMultipleFiles(styles); + + const overrides = { extractCss: true, styles: getStylesOption() }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // Check css files were created. + tap(() => Object.keys(cssMatches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(cssMatches[fileName]); + })), + // Check no js files are created. + tap(() => Object.keys(jsMatches).forEach(key => + expect(host.scopedSync().exists(normalize(key))).toBe(false), + )), + // Check check index has styles in the right order. + tap(() => Object.keys(cssIndexMatches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(cssIndexMatches[fileName]); + })), + // Also test with extractCss false. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: false, styles: getStylesOption() })), + // TODO: figure out why adding this tap breaks typings. + // This also happens in the output-hashing spec. + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // Check js files were created. + tap(() => Object.keys(jsMatches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(jsMatches[fileName]); + })), + // Check no css files are created. + tap(() => Object.keys(cssMatches).forEach(key => + expect(host.scopedSync().exists(normalize(key))).toBe(false), + )), + // Check check index has styles in the right order. + tap(() => Object.keys(jsIndexMatches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(jsIndexMatches[fileName]); + })), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('supports empty styleUrls in components', (done) => { + host.writeMultipleFiles({ + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: [] + }) + export class AppComponent { + title = 'app'; + } + `, + }); + + const overrides = { extractCss: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + extensionsWithImportSupport.forEach(ext => { + it(`supports imports in ${ext} files`, (done) => { + host.writeMultipleFiles({ + [`src/styles.${ext}`]: ` + @import './imported-styles.${ext}'; + body { background-color: #00f; } + `, + [`src/imported-styles.${ext}`]: 'p { background-color: #f00; }', + [`src/app/app.component.${ext}`]: ` + @import './imported-component-styles.${ext}'; + .outer { + .inner { + background: #fff; + } + } + `, + [`src/app/imported-component-styles.${ext}`]: 'h1 { background: #000; }', + }); + + const matches: { [path: string]: RegExp } = { + 'dist/styles.css': new RegExp( + // The global style should be there + /p\s*{\s*background-color: #f00;\s*}(.|\n|\r)*/.source + // The global style via import should be there + + /body\s*{\s*background-color: #00f;\s*}/.source, + ), + 'dist/styles.css.map': /"mappings":".+"/, + 'dist/main.js': new RegExp( + // The component style should be there + /h1(.|\n|\r)*background:\s*#000(.|\n|\r)*/.source + // The component style via import should be there + + /.outer(.|\n|\r)*.inner(.|\n|\r)*background:\s*#[fF]+/.source, + ), + }; + + const overrides = { + extractCss: true, + sourceMap: true, + styles: [`src/styles.${ext}`], + }; + + host.replaceInFile('src/app/app.component.ts', './app.component.css', + `./app.component.${ext}`); + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => Object.keys(matches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(matches[fileName]); + })), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + }); + + extensionsWithImportSupport.forEach(ext => { + it(`supports material imports in ${ext} files`, (done) => { + host.writeMultipleFiles({ + [`src/styles.${ext}`]: ` + @import "~@angular/material/prebuilt-themes/indigo-pink.css"; + @import "@angular/material/prebuilt-themes/indigo-pink.css"; + `, + [`src/app/app.component.${ext}`]: ` + @import "~@angular/material/prebuilt-themes/indigo-pink.css"; + @import "@angular/material/prebuilt-themes/indigo-pink.css"; + `, + }); + host.replaceInFile('src/app/app.component.ts', './app.component.css', + `./app.component.${ext}`); + + const overrides = { + extractCss: true, + styles: [{ input: `src/styles.${ext}` }], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + }); + + it(`supports material icons`, (done) => { + const overrides = { + extractCss: true, + optimization: true, + styles: [ + { input: '../../../../node_modules/material-design-icons/iconfont/material-icons.css' }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + extensionsWithVariableSupport.forEach(ext => { + it(`supports ${ext} includePaths`, (done) => { + + let variableAssignment = ''; + let variablereference = ''; + if (ext === 'scss') { + variableAssignment = '$primary-color:'; + variablereference = '$primary-color'; + } else if (ext === 'styl') { + variableAssignment = '$primary-color ='; + variablereference = '$primary-color'; + } else if (ext === 'less') { + variableAssignment = '@primary-color:'; + variablereference = '@primary-color'; + } + + host.writeMultipleFiles({ + [`src/style-paths/variables.${ext}`]: `${variableAssignment} #f00;`, + [`src/styles.${ext}`]: ` + @import 'variables'; + h1 { color: ${variablereference}; } + `, + [`src/app/app.component.${ext}`]: ` + @import 'variables'; + h2 { color: ${variablereference}; } + `, + }); + + const matches: { [path: string]: RegExp } = { + 'dist/styles.css': /h1\s*{\s*color: #f00;\s*}/, + 'dist/main.js': /h2.*{.*color: #f00;.*}/, + }; + + host.replaceInFile('src/app/app.component.ts', './app.component.css', + `./app.component.${ext}`); + + const overrides = { + extractCss: true, + styles: [`src/styles.${ext}`], + stylePreprocessorOptions: { + includePaths: ['src/style-paths'], + }, + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => Object.keys(matches).forEach(fileName => { + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(matches[fileName]); + })), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); + }); + + it('inlines resources', (done) => { + host.copyFile('src/spectrum.png', 'src/large.png'); + host.writeMultipleFiles({ + 'src/styles.scss': ` + h1 { background: url('./large.png'), + linear-gradient(to bottom, #0e40fa 25%, #0654f4 75%); } + h2 { background: url('./small.svg'); } + p { background: url(./small-id.svg#testID); } + `, + 'src/app/app.component.css': ` + h3 { background: url('../small.svg'); } + h4 { background: url("../large.png"); } + `, + 'src/small.svg': imgSvg, + 'src/small-id.svg': imgSvg, + }); + + const overrides = { + aot: true, + extractCss: true, + styles: [`src/styles.scss`], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + // Large image should not be inlined, and gradient should be there. + expect(content).toMatch( + /url\(['"]?large\.png['"]?\),\s+linear-gradient\(to bottom, #0e40fa 25%, #0654f4 75%\);/); + // Small image should be inlined. + expect(content).toMatch(/url\(\\?['"]data:image\/svg\+xml/); + // Small image with param should not be inlined. + expect(content).toMatch(/url\(['"]?small-id\.svg#testID['"]?\)/); + }), + tap(() => { + const fileName = 'dist/main.js'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + // Large image should not be inlined. + expect(content).toMatch(/url\((?:['"]|\\')?large\.png(?:['"]|\\')?\)/); + // Small image should be inlined. + expect(content).toMatch(/url\(\\?['"]data:image\/svg\+xml/); + }), + tap(() => { + expect(host.scopedSync().exists(normalize('dist/small.svg'))).toBe(false); + expect(host.scopedSync().exists(normalize('dist/large.png'))).toBe(true); + expect(host.scopedSync().exists(normalize('dist/small-id.svg'))).toBe(true); + }), + // TODO: find a way to check logger/output for warnings. + // if (stdout.match(/postcss-url: \.+: Can't read file '\.+', ignoring/)) { + // throw new Error('Expected no postcss-url file read warnings.'); + // } + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`supports font-awesome imports`, (done) => { + host.writeMultipleFiles({ + 'src/styles.scss': ` + $fa-font-path: "~font-awesome/fonts"; + @import "~font-awesome/scss/font-awesome"; + `, + }); + + const overrides = { extractCss: true, styles: [`src/styles.scss`] }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it(`uses autoprefixer`, (done) => { + host.writeMultipleFiles({ + 'src/styles.css': tags.stripIndents` + /* normal-comment */ + /*! important-comment */ + div { flex: 1 }`, + }); + + const overrides = { extractCss: true, optimization: false }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain(tags.stripIndents` + /* normal-comment */ + /*! important-comment */ + div { -ms-flex: 1; flex: 1 }`); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it(`minimizes css`, (done) => { + host.writeMultipleFiles({ + 'src/styles.css': tags.stripIndents` + /* normal-comment */ + /*! important-comment */ + div { flex: 1 }`, + }); + + const overrides = { extractCss: true, optimization: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = 'dist/styles.css'; + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toContain( + '/*! important-comment */div{-ms-flex:1;flex:1}'); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Standard); + + // TODO: consider making this a unit test in the url processing plugins. + it(`supports baseHref and deployUrl in resource urls`, (done) => { + // 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'); } + `, + // Use a small SVG for the absolute image to help validate that it is being referenced, + // because it is so small it would be inlined usually. + 'src/assets/global-img-absolute.svg': imgSvg, + 'src/assets/component-img-absolute.svg': imgSvg, + }); + + const stylesBundle = 'dist/styles.css'; + const mainBundle = 'dist/main.js'; + + // Check base paths are correctly generated. + runTargetSpec(host, browserTargetSpec, { aot: true, extractCss: true }).pipe( + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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/global-img-absolute.svg'))) + .toBe(false); + expect(host.scopedSync().exists(normalize('dist/global-img-relative.png'))) + .toBe(true); + expect(host.scopedSync().exists(normalize('dist/component-img-absolute.svg'))) + .toBe(false); + expect(host.scopedSync().exists(normalize('dist/component-img-relative.png'))) + .toBe(true); + }), + // Check urls with deploy-url scheme are used as is. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: true, baseHref: '/base/', deployUrl: 'http://deploy.url/' }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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). + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: true, baseHref: 'http://base.url/', deployUrl: 'deploy/' }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, { + extractCss: true, + baseHref: 'http://base.url/', + deployUrl: 'http://deploy.url/', + }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: true, baseHref: '/base/', deployUrl: 'deploy/' }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: true, baseHref: '/base/', deployUrl: '/base/' }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + 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. + concatMap(() => runTargetSpec(host, browserTargetSpec, + { extractCss: true, baseHref: '/base/' }, + )), + tap(() => { + const styles = virtualFs.fileBufferToString( + host.scopedSync().read(normalize(stylesBundle)), + ); + const main = virtualFs.fileBufferToString(host.scopedSync().read(normalize(mainBundle))); + expect(styles).toContain(`url('/base/assets/global-img-absolute.svg')`); + expect(main).toContain(`url('/base/assets/component-img-absolute.svg')`); + }), + ).subscribe(undefined, done.fail, done); + }, 90000); + + it(`supports bootstrap@4`, (done) => { + const overrides = { + extractCss: true, + styles: ['../../../../node_modules/bootstrap/dist/css/bootstrap.css'], + scripts: ['../../../../node_modules/bootstrap/dist/js/bootstrap.js'], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/subresource-integrity_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/subresource-integrity_spec_large.ts new file mode 100644 index 0000000000..c619e87f86 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/subresource-integrity_spec_large.ts @@ -0,0 +1,38 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder subresource integrity', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + // TODO: WEBPACK4_DISABLED - disabled pending a webpack 4 version + xit('works', (done) => { + 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 }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'index.html'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + expect(content).toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/tsconfig-paths_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/tsconfig-paths_spec_large.ts new file mode 100644 index 0000000000..0ebfdd91bd --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/tsconfig-paths_spec_large.ts @@ -0,0 +1,74 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder tsconfig paths', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + host.replaceInFile('src/app/app.module.ts', './app.component', '@root/app/app.component'); + host.replaceInFile('tsconfig.json', /"baseUrl": ".\/",/, ` + "baseUrl": "./", + "paths": { + "@root/*": [ + "./src/*" + ] + }, + `); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); + + it('works', (done) => { + host.writeMultipleFiles({ + 'src/meaning-too.ts': 'export var meaning = 42;', + 'src/app/shared/meaning.ts': 'export var meaning = 42;', + 'src/app/shared/index.ts': `export * from './meaning'`, + }); + host.replaceInFile('tsconfig.json', /"baseUrl": ".\/",/, ` + "baseUrl": "./", + "paths": { + "@shared": [ + "src/app/shared" + ], + "@shared/*": [ + "src/app/shared/*" + ], + "*": [ + "*", + "src/app/shared/*" + ] + }, + `); + host.appendToFile('src/app/app.component.ts', ` + import { meaning } from 'src/app/shared/meaning'; + import { meaning as meaning2 } from '@shared'; + import { meaning as meaning3 } from '@shared/meaning'; + import { meaning as meaning4 } from 'meaning'; + import { meaning as meaning5 } from 'src/meaning-too'; + + // need to use imports otherwise they are ignored and + // no error is outputted, even if baseUrl/paths don't work + console.log(meaning) + console.log(meaning2) + console.log(meaning3) + console.log(meaning4) + console.log(meaning5) + `); + + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/vendor-chunk_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/vendor-chunk_spec_large.ts new file mode 100644 index 0000000000..63862b161d --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/vendor-chunk_spec_large.ts @@ -0,0 +1,31 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder vendor chunk', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { vendorChunk: true }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = join(outputPath, 'vendor.js'); + expect(host.scopedSync().exists(fileName)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/browser/works_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/works_spec_large.ts new file mode 100644 index 0000000000..c7e20a5de8 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/browser/works_spec_large.ts @@ -0,0 +1,35 @@ +/** + * @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 { join, normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Browser Builder basic test', () => { + const outputPath = normalize('dist'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + runTargetSpec(host, browserTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + // Default files should be in outputPath. + expect(host.scopedSync().exists(join(outputPath, 'runtime.js'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'main.js'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'polyfills.js'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'styles.js'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'vendor.js'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'favicon.ico'))).toBe(true); + expect(host.scopedSync().exists(join(outputPath, 'index.html'))).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, Timeout.Basic); +}); diff --git a/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts new file mode 100644 index 0000000000..d3744c4ac7 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/dev-server/proxy_spec_large.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as express from 'express'; // tslint:disable-line:no-implicit-dependencies +import * as http from 'http'; +import { from } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; +import { DevServerBuilderOptions } from '../../src'; +import { devServerTargetSpec, host, request, runTargetSpec } from '../utils'; + + +describe('Dev Server Builder proxy', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + // Create an express app that serves as a proxy. + const app = express(); + const server = http.createServer(app); + server.listen(0); + + app.set('port', server.address().port); + app.get('/api/test', function (_req, res) { + res.send('TEST_API_RETURN'); + }); + + const backendHost = 'localhost'; + const backendPort = server.address().port; + const proxyServerUrl = `http://${backendHost}:${backendPort}`; + + host.writeMultipleFiles({ + 'proxy.config.json': `{ "/api/*": { "target": "${proxyServerUrl}" } }`, + }); + + const overrides: Partial = { proxyConfig: 'proxy.config.json' }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/api/test'))), + tap(response => { + expect(response).toContain('TEST_API_RETURN'); + server.close(); + }), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('errors out with a missing proxy file', (done) => { + const overrides: Partial = { proxyConfig: '../proxy.config.json' }; + + runTargetSpec(host, devServerTargetSpec, overrides) + .subscribe(undefined, done, done.fail); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts new file mode 100644 index 0000000000..3d1b3eaa5a --- /dev/null +++ b/packages/angular_devkit/build_angular/test/dev-server/public-host_spec_large.ts @@ -0,0 +1,53 @@ +/** + * @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 { from } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; +import { DevServerBuilderOptions } from '../../src'; +import { devServerTargetSpec, host, request, runTargetSpec } from '../utils'; + + +describe('Dev Server Builder public host', () => { + // We have to spoof the host to a non-numeric one because Webpack Dev Server does not + // check the hosts anymore when requests come from numeric IP addresses. + const headers = { host: 'http://spoofy.mcspoofface' }; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + runTargetSpec(host, devServerTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/', headers))), + tap(response => expect(response).toContain('Invalid Host header')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('works', (done) => { + const overrides: Partial = { publicHost: headers.host }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/', headers))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('works', (done) => { + const overrides: Partial = { disableHostCheck: true }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/', headers))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts new file mode 100644 index 0000000000..f0e16dbf8c --- /dev/null +++ b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts @@ -0,0 +1,32 @@ +/** + * @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 { from } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; +import { DevServerBuilderOptions } from '../../src'; +import { devServerTargetSpec, host, request, runTargetSpec } from '../utils'; + + +describe('Dev Server Builder serve path', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + // TODO: review this test, it seems to pass with or without the servePath. + it('works', (done) => { + const overrides: Partial = { servePath: 'test/' }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/test/'))), + tap(response => expect(response).toContain('HelloWorldApp')), + concatMap(() => from(request('http://localhost:4200/test/abc/'))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts new file mode 100644 index 0000000000..cbc7e82817 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/dev-server/ssl_spec_large.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { tags } from '@angular-devkit/core'; +import { from } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; +import { DevServerBuilderOptions } from '../../src'; +import { devServerTargetSpec, host, request, runTargetSpec } from '../utils'; + + +describe('Dev Server Builder ssl', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides: Partial = { ssl: true }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('https://localhost:4200/index.html'))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports key and cert', (done) => { + host.writeMultipleFiles({ + 'ssl/server.key': tags.stripIndents` + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA0+kOHdfpsPNUguCtai27DJ+DuOfVw4gw1P3BgYhNG8qoukaW + gdDLowj59+cXXgn9ZnQ8PdvZXhCEQcP3wjd1cBPuGQzO7BekoKEgzfGE/zjrrIeL + Q7tHYx9ddCPotQX4OJu4FZFJyS1Ar7zDDpQ9fSw6qmsXWN6I18fIGSVvbDbVB9rw + iAsLGe2jWadjXAjSqVOy6aT+CKJgnRnxudNZGP1LRC1YDRl/s7icCIh/9gEGfn7G + gwQQ9AM2p3JNjP2quclSjBvMv0uVj+yzL2bPGRz0hu90CDaIEU2FtBCse50/O75q + x0bQnPbuHKD3ajs0DfQYoAmFZGK078ZDl/VQxwIDAQABAoIBAEl17kXcNo/4GqDw + QE2hoslCdwhfnhQVn1AG09ESriBnRcylccF4308aaoVM4CXicqzUuJl9IEJimWav + B7GVRinfTtfyP71KiPCCSvv5sPBFDDYYGugVAS9UjTIYzLAMbLs7CDq5zglmnZkO + Z9QjAZnl/kRbsZFGO8wJ3s0Q1Cp/ygZcvFU331K2jHXW7B4YXiFOH/lBQrjdz0Gy + WBjX4zIdNWnwarvxu46IS/0z1P1YOHM8+B1Uv54MG94A6szBdd/Vp0cQRs78t/Cu + BQ1Rnuk16Pi+ieC5K04yUgeuNusYW0PWLtPX1nKNp9z46bmD1NHKAxaoDFXr7qP3 + pZCaDMkCgYEA8mmTYrhXJTRIrOxoUwM1e3OZ0uOxVXJJ8HF6X8t+UO6dFxXB/JC9 + ZBc+94cZQapaKFOeMmd/j3L2CQIjChk5yKV/G3Io+raxIoAAKPCkMF4NQQVvvNkS + CAGl61Qa78DoF5Habumz0AC1R9P877kNTC0aPSt4lhPWgfotbZNNMlMCgYEA38nM + s4a0pZseXPkuOtPYX/3Ms3E+d70XKSFuIMCHCg79YGsQ8h/9apYcPyeYkpQ0a4gs + I3IUqMaXC2OyqWA5LU1BZv51mXb6zcb2pokZfpiSWk+7sy5XjkE9EmQxp3xHfV3c + EO/DxHfWNvtMjESMbhu0yVzM2O/Aa53Tl9lqAT0CgYEA1dXBuHyqCtyTG08zO78B + 55Ny5rAJ1zkI9jvz2hr0o0nJcvqzcyruliNXXRxkcCNoglg4nXfk81JSrGGhLSBR + c6hhdoF+mqKboLZO7c5Q14WvpWK5TVoiaMOja/J2DHYbhecYS2yGPH7TargaUBDq + JP9IPRtitOhs+Z0Jg7ZDi5cCgYAMb7B6gY/kbBxh2k8hYchyfS41AqQQD2gMFxmB + pHFcs7yM8SY97l0s4S6sq8ykyKupFiYtyhcv0elu7pltJDXJOLPbv2RVpPEHInlu + g8vw5xWrAydRK9Adza5RKVRBFHz8kIy8PDbK4kX7RDfay6xqKgv/7LJNk/VDhb/O + fnyPmQKBgQDg/o8Ubf/gxA9Husnuld4DBu3wwFhkMlWqyO9QH3cKgojQ2JGSrfDz + xHhetmhionEyzg0JCaMSpzgIHY+8o/NAwc++OjNHEoYp3XWM9GTp81ROMz6b83jV + biVR9N0MhONdwF6vtzDCcJxNIUe2p4lTvLf/Xd9jaQDNXe35Gxsdyg== + -----END RSA PRIVATE KEY----- + `, + 'ssl/server.crt': tags.stripIndents` + -----BEGIN CERTIFICATE----- + MIID5jCCAs6gAwIBAgIJAJOebwfGCm61MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV + BAYTAlVTMRAwDgYDVQQIEwdHZW9yZ2lhMRAwDgYDVQQHEwdBdGxhbnRhMRAwDgYD + VQQKEwdBbmd1bGFyMRAwDgYDVQQLEwdBbmd1bGFyMB4XDTE2MTAwNDAxMDAyMVoX + DTI2MTAwMjAxMDAyMVowVTELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0dlb3JnaWEx + EDAOBgNVBAcTB0F0bGFudGExEDAOBgNVBAoTB0FuZ3VsYXIxEDAOBgNVBAsTB0Fu + Z3VsYXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDT6Q4d1+mw81SC + 4K1qLbsMn4O459XDiDDU/cGBiE0byqi6RpaB0MujCPn35xdeCf1mdDw929leEIRB + w/fCN3VwE+4ZDM7sF6SgoSDN8YT/OOush4tDu0djH110I+i1Bfg4m7gVkUnJLUCv + vMMOlD19LDqqaxdY3ojXx8gZJW9sNtUH2vCICwsZ7aNZp2NcCNKpU7LppP4IomCd + GfG501kY/UtELVgNGX+zuJwIiH/2AQZ+fsaDBBD0Azanck2M/aq5yVKMG8y/S5WP + 7LMvZs8ZHPSG73QINogRTYW0EKx7nT87vmrHRtCc9u4coPdqOzQN9BigCYVkYrTv + xkOX9VDHAgMBAAGjgbgwgbUwHQYDVR0OBBYEFG4VV6/aNLx/qFIS9MhAWuyeV5OX + MIGFBgNVHSMEfjB8gBRuFVev2jS8f6hSEvTIQFrsnleTl6FZpFcwVTELMAkGA1UE + BhMCVVMxEDAOBgNVBAgTB0dlb3JnaWExEDAOBgNVBAcTB0F0bGFudGExEDAOBgNV + BAoTB0FuZ3VsYXIxEDAOBgNVBAsTB0FuZ3VsYXKCCQCTnm8HxgputTAMBgNVHRME + BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDO4jZT/oKVxaiWr+jV5TD+qwThl9zT + Uw/ZpFDkdbZdY/baCFaLCiJwkK9+puMOabLvm1VzcnHHWCoiUNbWpw8AOumLEnTv + ze/5OZXJ6XlA9kd9f3hDlN5zNB3S+Z2nKIrkPGfxQZ603QCbWaptip5dxgek6oDZ + YXVtnbOnPznRsG5jh07U49RO8CNebqZLzdRToLgObbqYlfRMcbUxCOHXjnB5wUlp + 377Iivm4ldnCTvFOjEiDh+FByWL5xic7PjyJPZFMidiYTmsGilP9XTFC83CRZwz7 + vW+RCSlU6x8Uejz98BPmASoqCuCTUeOo+2pFelFhX9NwR/Sb6b7ybdPv + -----END CERTIFICATE----- + `, + }); + + const overrides: Partial = { + ssl: true, + sslKey: '../ssl/server.key', + sslCert: '../ssl/server.crt', + }; + + runTargetSpec(host, devServerTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('https://localhost:4200/index.html'))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts new file mode 100644 index 0000000000..da5e4bc0ec --- /dev/null +++ b/packages/angular_devkit/build_angular/test/dev-server/works_spec_large.ts @@ -0,0 +1,26 @@ +/** + * @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 { from } from 'rxjs'; +import { concatMap, take, tap } from 'rxjs/operators'; +import { devServerTargetSpec, host, request, runTargetSpec } from '../utils'; + + +describe('Dev Server Builder', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + runTargetSpec(host, devServerTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + concatMap(() => from(request('http://localhost:4200/index.html'))), + tap(response => expect(response).toContain('HelloWorldApp')), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts b/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts new file mode 100644 index 0000000000..6d35a786bb --- /dev/null +++ b/packages/angular_devkit/build_angular/test/extract-i18n/works_spec_large.ts @@ -0,0 +1,108 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { TestLogger, extractI18nTargetSpec, host, runTargetSpec } from '../utils'; + + +describe('Extract i18n Target', () => { + const extractionFile = join(normalize('src'), 'messages.xlf'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + host.appendToFile('src/app/app.component.html', '

i18n test

'); + + runTargetSpec(host, extractI18nTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists((extractionFile))).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('shows errors', (done) => { + const logger = new TestLogger('i18n-errors'); + host.appendToFile('src/app/app.component.html', + '

Hello world inner

'); + + runTargetSpec(host, extractI18nTargetSpec, {}, logger).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(false); + const msg = 'Could not mark an element as translatable inside a translatable section'; + expect(logger.includes(msg)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports locale', (done) => { + host.appendToFile('src/app/app.component.html', '

i18n test

'); + const overrides = { i18nLocale: 'fr' }; + + runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists((extractionFile))).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toContain('source-language="fr"'); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports out file', (done) => { + host.appendToFile('src/app/app.component.html', '

i18n test

'); + const outFile = 'messages.fr.xlf'; + const extractionFile = join(normalize('src'), outFile); + const overrides = { outFile }; + + runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports output path', (done) => { + host.appendToFile('src/app/app.component.html', '

i18n test

'); + // Note: this folder will not be created automatically. It must exist beforehand. + const outputPath = 'app'; + const extractionFile = join(normalize('src'), outputPath, 'messages.xlf'); + const overrides = { outputPath }; + + runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports i18n format', (done) => { + host.appendToFile('src/app/app.component.html', '

i18n test

'); + const extractionFile = join(normalize('src'), 'messages.xmb'); + const overrides = { i18nFormat: 'xmb' }; + + runTargetSpec(host, extractI18nTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(extractionFile)).toBe(true); + expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile))) + .toMatch(/i18n test/); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/karma/assets_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/assets_spec_large.ts new file mode 100644 index 0000000000..2dd1b5d192 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/karma/assets_spec_large.ts @@ -0,0 +1,100 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { host, karmaTargetSpec, runTargetSpec } from '../utils'; + + +describe('Karma Builder assets', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const assets: { [path: string]: string } = { + './src/folder/folder-asset.txt': 'folder-asset.txt', + './src/glob-asset.txt': 'glob-asset.txt', + './src/output-asset.txt': 'output-asset.txt', + }; + host.writeMultipleFiles(assets); + host.writeMultipleFiles({ + 'src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { HttpModule } from '@angular/http'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + HttpModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + 'src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { Http, Response } from '@angular/http'; + + @Component({ + selector: 'app-root', + template: '

{{asset.content }}

' + }) + export class AppComponent { + public assets = [ + { path: './folder/folder-asset.txt', content: '' }, + { path: './glob-asset.txt', content: '' }, + { path: './output-folder/output-asset.txt', content: '' }, + ]; + constructor(private http: Http) { + this.assets.forEach(asset => http.get(asset.path) + .subscribe(res => asset.content = res['_body'])); + } + }`, + 'src/app/app.component.spec.ts': ` + import { TestBed, async } from '@angular/core/testing'; + import { HttpModule } from '@angular/http'; + import { AppComponent } from './app.component'; + + describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule + ], + declarations: [ + AppComponent + ] + }).compileComponents(); + })); + + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + });`, + }); + + const overrides = { + assets: [ + { glob: 'glob-asset.txt', input: 'src/', output: '/' }, + { glob: 'output-asset.txt', input: 'src/', output: '/output-folder' }, + { glob: '**/*', input: 'src/folder', output: '/folder' }, + ], + }; + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + tap(buildEvent => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 45000); +}); diff --git a/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts new file mode 100644 index 0000000000..9365ca8085 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts @@ -0,0 +1,58 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { debounceTime, tap } from 'rxjs/operators'; +import { KarmaBuilderOptions } from '../../src'; +import { host, karmaTargetSpec, runTargetSpec } from '../utils'; + + +describe('Karma Builder code coverage', () => { + const coverageFilePath = normalize('coverage/lcov.info'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides: Partial = { codeCoverage: true }; + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + // It seems like the coverage files take a while being written to disk, so we wait 500ms here. + debounceTime(500), + tap(buildEvent => { + expect(buildEvent.success).toBe(true); + expect(host.scopedSync().exists(coverageFilePath)).toBe(true); + const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); + expect(content).toContain('polyfills.ts'); + expect(content).toContain('test.ts'); + }), + ).subscribe(undefined, done.fail, done); + }, 120000); + + it('supports exclude', (done) => { + const overrides: Partial = { + codeCoverage: true, + codeCoverageExclude: [ + 'src/polyfills.ts', + '**/test.ts', + ], + }; + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + // It seems like the coverage files take a while being written to disk, so we wait 500ms here. + debounceTime(500), + tap(buildEvent => { + expect(buildEvent.success).toBe(true); + expect(host.scopedSync().exists(coverageFilePath)).toBe(true); + const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); + expect(content).not.toContain('polyfills.ts'); + expect(content).not.toContain('test.ts'); + }), + ).subscribe(undefined, done.fail, done); + }, 120000); +}); diff --git a/packages/angular_devkit/build_angular/test/karma/rebuilds_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/rebuilds_spec_large.ts new file mode 100644 index 0000000000..71f8acd518 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/karma/rebuilds_spec_large.ts @@ -0,0 +1,66 @@ +/** + * @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 { debounceTime, take, tap } from 'rxjs/operators'; +import { host, karmaTargetSpec, runTargetSpec } from '../utils'; + + +// Karma watch mode is currently bugged: +// - errors print a huge stack trace +// - karma does not have a way to close the server gracefully. +// TODO: fix these before 6.0 final. +xdescribe('Karma Builder watch mode', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + const overrides = { watch: true }; + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + debounceTime(500), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + take(1), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('recovers from compilation failures in watch mode', (done) => { + const overrides = { watch: true }; + let buildNumber = 0; + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + debounceTime(500), + tap((buildEvent) => { + buildNumber += 1; + switch (buildNumber) { + case 1: + // Karma run should succeed. + // Add a compilation error. + expect(buildEvent.success).toBe(true); + host.writeMultipleFiles({ + 'src/app/app.component.spec.ts': '

definitely not typescript

', + }); + break; + + case 2: + // Karma run should fail due to compilation error. Fix it. + expect(buildEvent.success).toBe(false); + host.writeMultipleFiles({ 'src/foo.spec.ts': '' }); + break; + + case 3: + // Karma run should succeed again. + expect(buildEvent.success).toBe(true); + break; + + default: + break; + } + }), + take(3), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/karma/replacements_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/replacements_spec_large.ts new file mode 100644 index 0000000000..584c363f75 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/karma/replacements_spec_large.ts @@ -0,0 +1,45 @@ +/** + * @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 { normalize } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { host, karmaTargetSpec, runTargetSpec } from '../utils'; + + +describe('Karma Builder file replacements', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('allows file replacements', (done) => { + host.writeMultipleFiles({ + 'src/meaning-too.ts': 'export var meaning = 42;', + 'src/meaning.ts': `export var meaning = 10;`, + + 'src/test.ts': ` + import { meaning } from './meaning'; + + describe('Test file replacement', () => { + it('should replace file', () => { + expect(meaning).toBe(42); + }); + }); + `, + }); + + const overrides = { + fileReplacements: [{ + replace: normalize('/src/meaning.ts'), + with: normalize('/src/meaning-too.ts'), + }], + }; + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/karma/works_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/works_spec_large.ts new file mode 100644 index 0000000000..2450482c01 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/karma/works_spec_large.ts @@ -0,0 +1,38 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { host, karmaTargetSpec, runTargetSpec } from '../utils'; + + +describe('Karma Builder', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('runs', (done) => { + runTargetSpec(host, karmaTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('fails with broken compilation', (done) => { + host.writeMultipleFiles({ + 'src/app/app.component.spec.ts': '

definitely not typescript

', + }); + runTargetSpec(host, karmaTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports ES2015 target', (done) => { + host.replaceInFile('tsconfig.json', '"target": "es5"', '"target": "es2015"'); + runTargetSpec(host, karmaTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/protractor/works_spec_large.ts b/packages/angular_devkit/build_angular/test/protractor/works_spec_large.ts new file mode 100644 index 0000000000..ae86677ab9 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/protractor/works_spec_large.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { normalize } from '@angular-devkit/core'; +import { retry } from 'rxjs/operators'; +import { host, protractorTargetSpec, runTargetSpec } from '../utils'; + + +// TODO: replace this with an "it()" macro that's reusable globally. +let linuxOnlyIt: typeof it = it; +if (process.platform.startsWith('win')) { + linuxOnlyIt = xit; +} + + +describe('Protractor Builder', () => { + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + linuxOnlyIt('works', (done) => { + runTargetSpec(host, protractorTargetSpec).pipe( + retry(3), + ).subscribe(undefined, done.fail, done); + }, 30000); + + linuxOnlyIt('works with no devServerTarget', (done) => { + const overrides = { devServerTarget: undefined }; + + runTargetSpec(host, protractorTargetSpec, overrides).pipe( + // This should fail because no server is available for connection. + ).subscribe(undefined, done, done.fail); + }, 30000); + + linuxOnlyIt('overrides protractor specs', (done) => { + host.scopedSync().rename(normalize('./e2e/app.e2e-spec.ts'), + normalize('./e2e/renamed-app.e2e-spec.ts')); + + const overrides = { specs: ['./e2e/renamed-app.e2e-spec.ts'] }; + + runTargetSpec(host, protractorTargetSpec, overrides).pipe( + retry(3), + ).subscribe(undefined, done.fail, done); + }, 60000); + + linuxOnlyIt('overrides protractor suites', (done) => { + host.scopedSync().rename(normalize('./e2e/app.e2e-spec.ts'), + normalize('./e2e/renamed-app.e2e-spec.ts')); + + // Suites block need to be added in the protractor.conf.js file to test suites + host.replaceInFile('protractor.conf.js', `allScriptsTimeout: 11000,`, ` + allScriptsTimeout: 11000, + suites: { + app: './e2e/app.e2e-spec.ts' + }, + `); + + const overrides = { suite: 'app' }; + + runTargetSpec(host, protractorTargetSpec, overrides).pipe( + retry(3), + ).subscribe(undefined, done.fail, done); + }, 60000); + + // TODO: test `element-explorer` when the protractor builder emits build events with text. + // .then(() => execAndWaitForOutputToMatch('ng', ['e2e', '--element-explorer'], + // /Element Explorer/)) + // .then(() => killAllProcesses(), (err: any) => { + // killAllProcesses(); + // throw err; + // }) +}); diff --git a/packages/angular_devkit/build_angular/test/server/base_spec_large.ts b/packages/angular_devkit/build_angular/test/server/base_spec_large.ts new file mode 100644 index 0000000000..159c1bd7a9 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/server/base_spec_large.ts @@ -0,0 +1,33 @@ +/** + * @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 { join, normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { host, runTargetSpec } from '../utils'; + + +describe('Server Builder', () => { + const outputPath = normalize('dist-server'); + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works (base)', (done) => { + const overrides = { }; + + runTargetSpec(host, { project: 'app', target: 'server' }, overrides).pipe( + tap((buildEvent) => { + expect(buildEvent.success).toBe(true); + + const fileName = join(outputPath, 'main.js'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); + expect(content).toMatch(/AppServerModuleNgFactory/); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/tslint/works_spec_large.ts b/packages/angular_devkit/build_angular/test/tslint/works_spec_large.ts new file mode 100644 index 0000000000..2b37c8991b --- /dev/null +++ b/packages/angular_devkit/build_angular/test/tslint/works_spec_large.ts @@ -0,0 +1,169 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { tap } from 'rxjs/operators'; +import { TslintBuilderOptions } from '../../src'; +import { TestLogger, host, runTargetSpec, tslintTargetSpec } from '../utils'; + + +describe('Tslint Target', () => { + const filesWithErrors = { 'src/foo.ts': 'const foo = "";\n' }; + + beforeEach(done => host.initialize().subscribe(undefined, done.fail, done)); + afterEach(done => host.restore().subscribe(undefined, done.fail, done)); + + it('works', (done) => { + runTargetSpec(host, tslintTargetSpec).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports exclude', (done) => { + host.writeMultipleFiles(filesWithErrors); + const overrides: Partial = { exclude: ['**/foo.ts'] }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports fix', (done) => { + host.writeMultipleFiles(filesWithErrors); + const overrides: Partial = { fix: true }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + const fileName = normalize('src/foo.ts'); + const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + expect(content).toContain(`const foo = '';`); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports force', (done) => { + host.writeMultipleFiles(filesWithErrors); + const logger = new TestLogger('lint-force'); + const overrides: Partial = { force: true }; + + runTargetSpec(host, tslintTargetSpec, overrides, logger).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(logger.includes(`" should be '`)).toBe(true); + expect(logger.includes(`Lint errors found in the listed files`)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports format', (done) => { + host.writeMultipleFiles(filesWithErrors); + const logger = new TestLogger('lint-format'); + const overrides: Partial = { format: 'stylish' }; + + runTargetSpec(host, tslintTargetSpec, overrides, logger).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(false)), + tap(() => { + expect(logger.includes(`quotemark`)).toBe(true); + }), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports finding configs', (done) => { + host.writeMultipleFiles({ + 'src/app/foo/foo.ts': `const foo = '';\n`, + 'src/app/foo/tslint.json': ` + { + "rules": { + "quotemark": [ + true, + "double" + ] + } + } + `, + }); + const overrides: Partial = { tslintConfig: undefined }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(false)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports overriding configs', (done) => { + host.writeMultipleFiles({ + 'src/app/foo/foo.ts': `const foo = '';\n`, + 'src/app/foo/tslint.json': ` + { + "rules": { + "quotemark": [ + true, + "double" + ] + } + } + `, + }); + const overrides: Partial = { tslintConfig: 'tslint.json' }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports using files with no project', (done) => { + const overrides: Partial = { + tsConfig: undefined, + files: ['src/app/**/*.ts'], + }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports using one project as a string', (done) => { + const overrides: Partial = { + tsConfig: 'src/tsconfig.app.json', + }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports using one project as an array', (done) => { + const overrides: Partial = { + tsConfig: ['src/tsconfig.app.json'], + }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('supports using two projects', (done) => { + const overrides: Partial = { + tsConfig: ['src/tsconfig.app.json', 'src/tsconfig.spec.json'], + }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('errors when type checking is used without a project', (done) => { + const overrides: Partial = { + tsConfig: undefined, + typeCheck: true, + }; + + runTargetSpec(host, tslintTargetSpec, overrides).pipe( + ).subscribe(undefined, done, done.fail); + }, 30000); +}); diff --git a/packages/angular_devkit/build_angular/test/utils/index.ts b/packages/angular_devkit/build_angular/test/utils/index.ts new file mode 100644 index 0000000000..807bf9c281 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './request'; +export * from './test-project-host'; +export * from './test-logger'; +export * from './timeouts'; +export * from './run-target-spec'; diff --git a/packages/angular_devkit/build_angular/test/utils/request.ts b/packages/angular_devkit/build_angular/test/utils/request.ts new file mode 100644 index 0000000000..9d0d0be91c --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/request.ts @@ -0,0 +1,29 @@ +/** + * @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 { IncomingMessage } from 'http'; +import * as _request from 'request'; + +export function request(url: string, headers = {}): Promise { + return new Promise((resolve, reject) => { + const options = { + url: url, + headers: { 'Accept': 'text/html', ...headers }, + agentOptions: { rejectUnauthorized: false }, + }; + // tslint:disable-next-line:no-any + _request(options, (error: any, response: IncomingMessage, body: string) => { + if (error) { + reject(error); + } else if (response.statusCode && response.statusCode >= 400) { + reject(new Error(`Requesting "${url}" returned status code ${response.statusCode}.`)); + } else { + resolve(body); + } + }); + }); +} diff --git a/packages/angular_devkit/build_angular/test/utils/run-target-spec.ts b/packages/angular_devkit/build_angular/test/utils/run-target-spec.ts new file mode 100644 index 0000000000..4d4197d0d6 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/run-target-spec.ts @@ -0,0 +1,43 @@ +/** + * @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, BuildEvent, TargetSpecifier } from '@angular-devkit/architect'; +import { experimental, join, logging, normalize } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import { TestProjectHost } from '../utils/test-project-host'; + + +const workspaceFile = normalize('.angular.json'); +const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any + +export const workspaceRoot = join(devkitRoot, + 'tests/@angular_devkit/build_angular/hello-world-app/'); +export const host = new TestProjectHost(workspaceRoot); +export const outputPath = normalize('dist'); +export const browserTargetSpec = { project: 'app', target: 'build' }; +export const devServerTargetSpec = { project: 'app', target: 'serve' }; +export const extractI18nTargetSpec = { project: 'app', target: 'extract-i18n' }; +export const karmaTargetSpec = { project: 'app', target: 'test' }; +export const tslintTargetSpec = { project: 'app', target: 'lint' }; +export const protractorTargetSpec = { project: 'app-e2e', target: 'e2e' }; + +export function runTargetSpec( + host: TestProjectHost, + targetSpec: TargetSpecifier, + overrides = {}, + logger: logging.Logger = new logging.NullLogger(), +): Observable { + targetSpec = { ...targetSpec, overrides }; + const workspace = new experimental.workspace.Workspace(workspaceRoot, host); + + return workspace.loadWorkspaceFromHost(workspaceFile).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + concatMap(arch => arch.run(arch.getBuilderConfiguration(targetSpec), { logger })), + ); +} diff --git a/packages/angular_devkit/build_angular/test/utils/test-logger.ts b/packages/angular_devkit/build_angular/test/utils/test-logger.ts new file mode 100644 index 0000000000..cda17fad25 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/test-logger.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { logging } from '@angular-devkit/core'; + + +export class TestLogger extends logging.Logger { + private _latestEntries: logging.LogEntry[] = []; + constructor(name: string, parent: logging.Logger | null = null) { + super(name, parent); + this.subscribe((entry) => this._latestEntries.push(entry)); + } + + clear() { + this._latestEntries = []; + } + + includes(message: string) { + return this._latestEntries.some((entry) => entry.message.includes(message)); + } + + test(re: RegExp) { + return this._latestEntries.some((entry) => re.test(entry.message)); + } +} diff --git a/packages/angular_devkit/build_angular/test/utils/test-project-host.ts b/packages/angular_devkit/build_angular/test/utils/test-project-host.ts new file mode 100644 index 0000000000..87a84704ed --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/test-project-host.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + Path, + getSystemPath, + normalize, + virtualFs, +} from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { SpawnOptions, spawn } from 'child_process'; +import { Stats } from 'fs'; +import { EMPTY, Observable } from 'rxjs'; +import { concatMap, map } from 'rxjs/operators'; + + +interface ProcessOutput { + stdout: string; + stderr: string; +} + +export class TestProjectHost extends NodeJsSyncHost { + private _scopedSyncHost: virtualFs.SyncDelegateHost; + + constructor(protected _root: Path) { + super(); + this._scopedSyncHost = new virtualFs.SyncDelegateHost(new virtualFs.ScopedHost(this, _root)); + } + + scopedSync() { + return this._scopedSyncHost; + } + + initialize(): Observable { + return this.exists(normalize('.git')).pipe( + concatMap(exists => !exists ? this._gitInit() : EMPTY), + ); + } + + restore(): Observable { + return this._gitClean(); + } + + private _gitClean(): Observable { + return this._exec('git', ['clean', '-fd']).pipe( + concatMap(() => this._exec('git', ['checkout', '.'])), + map(() => { }), + ); + } + + private _gitInit(): Observable { + return this._exec('git', ['init']).pipe( + concatMap(() => this._exec('git', ['config', 'user.email', 'angular-core+e2e@google.com'])), + concatMap(() => this._exec('git', ['config', 'user.name', 'Angular DevKit Tests'])), + concatMap(() => this._exec('git', ['add', '--all'])), + concatMap(() => this._exec('git', ['commit', '-am', '"Initial commit"'])), + map(() => { }), + ); + } + + private _exec(cmd: string, args: string[]): Observable { + return new Observable(obs => { + args = args.filter(x => x !== undefined); + let stdout = ''; + let stderr = ''; + + const spawnOptions: SpawnOptions = { cwd: getSystemPath(this._root) }; + + if (process.platform.startsWith('win')) { + args.unshift('/c', cmd); + cmd = 'cmd.exe'; + spawnOptions['stdio'] = 'pipe'; + } + + const childProcess = spawn(cmd, args, spawnOptions); + childProcess.stdout.on('data', (data: Buffer) => stdout += data.toString('utf-8')); + childProcess.stderr.on('data', (data: Buffer) => stderr += data.toString('utf-8')); + + // Create the error here so the stack shows who called this function. + const err = new Error(`Running "${cmd} ${args.join(' ')}" returned error code `); + + childProcess.on('exit', (code) => { + if (!code) { + obs.next({ stdout, stderr }); + } else { + err.message += `${code}.\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}\n`; + obs.error(err); + } + obs.complete(); + }); + }); + } + + writeMultipleFiles(files: { [path: string]: string | ArrayBufferLike | Buffer }): void { + Object.keys(files).map(fileName => { + let content = files[fileName]; + if (typeof content == 'string') { + content = virtualFs.stringToFileBuffer(content); + } else if (content instanceof Buffer) { + content = content.buffer.slice( + content.byteOffset, + content.byteOffset + content.byteLength, + ); + } + + this.scopedSync().write( + normalize(fileName), + content, + ); + }); + } + + replaceInFile(path: string, match: RegExp | string, replacement: string) { + const content = virtualFs.fileBufferToString(this.scopedSync().read(normalize(path))); + this.scopedSync().write(normalize(path), + virtualFs.stringToFileBuffer(content.replace(match, replacement))); + } + + appendToFile(path: string, str: string) { + const content = virtualFs.fileBufferToString(this.scopedSync().read(normalize(path))); + this.scopedSync().write(normalize(path), + virtualFs.stringToFileBuffer(content.concat(str))); + } + + fileMatchExists(dir: string, regex: RegExp) { + const [fileName] = this.scopedSync().list(normalize(dir)).filter(name => name.match(regex)); + + return fileName || undefined; + } + + copyFile(from: string, to: string) { + const content = this.scopedSync().read(normalize(from)); + this.scopedSync().write(normalize(to), content); + } +} diff --git a/packages/angular_devkit/build_angular/test/utils/timeouts.ts b/packages/angular_devkit/build_angular/test/utils/timeouts.ts new file mode 100644 index 0000000000..46e0fbfd23 --- /dev/null +++ b/packages/angular_devkit/build_angular/test/utils/timeouts.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export enum Timeout { + Basic = 30000, + Standard = Basic * 1.5, + Complex = Basic * 2, + Massive = Basic * 4, +} diff --git a/packages/angular_devkit/build_ng_packagr/README.md b/packages/angular_devkit/build_ng_packagr/README.md new file mode 100644 index 0000000000..acfeaea954 --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/README.md @@ -0,0 +1,3 @@ +# Angular Build Architect for ng-packagr + +WIP \ No newline at end of file diff --git a/packages/angular_devkit/build_ng_packagr/builders.json b/packages/angular_devkit/build_ng_packagr/builders.json new file mode 100644 index 0000000000..58db53b1c4 --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/builders.json @@ -0,0 +1,10 @@ +{ + "$schema": "../architect/src/builders-schema.json", + "builders": { + "build": { + "class": "./src/build", + "schema": "./src/build/schema.json", + "description": "Build a library with ng-packagr." + } + } +} diff --git a/packages/angular_devkit/build_ng_packagr/package.json b/packages/angular_devkit/build_ng_packagr/package.json new file mode 100644 index 0000000000..15c8441625 --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/package.json @@ -0,0 +1,20 @@ +{ + "name": "@angular-devkit/build-ng-packagr", + "version": "0.0.0", + "description": "Angular Build Architect for ng-packagr", + "main": "src/index.js", + "typings": "src/index.d.ts", + "builders": "builders.json", + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "dependencies": { + "@angular-devkit/architect": "0.0.0", + "@angular-devkit/core": "0.0.0", + "resolve": "^1.5.0", + "rxjs": "^6.0.0-beta.3" + }, + "peerDependencies": { + "ng-packagr": "^2.2.0" + } +} \ No newline at end of file diff --git a/packages/angular_devkit/build_ng_packagr/src/build/index.ts b/packages/angular_devkit/build_ng_packagr/src/build/index.ts new file mode 100644 index 0000000000..20a3d672e3 --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/src/build/index.ts @@ -0,0 +1,62 @@ +/** + * @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 { + BuildEvent, + Builder, + BuilderConfiguration, + BuilderContext, +} from '@angular-devkit/architect'; +import { getSystemPath, normalize, resolve } from '@angular-devkit/core'; +import * as ngPackagr from 'ng-packagr'; +import { Observable } from 'rxjs'; + +// TODO move this function to architect or somewhere else where it can be imported from. +// Blatantly copy-pasted from 'require-project-module.ts'. +function requireProjectModule(root: string, moduleName: string) { + const resolve = require('resolve'); + + return require(resolve.sync(moduleName, { basedir: root })); +} + + +export interface NgPackagrBuilderOptions { + project: string; +} + +export class NgPackagrBuilder implements Builder { + + constructor(public context: BuilderContext) { } + + run(builderConfig: BuilderConfiguration): Observable { + const root = this.context.workspace.root; + const options = builderConfig.options; + + if (!options.project) { + throw new Error('A "project" must be specified to build a library\'s npm package.'); + } + + return new Observable(obs => { + const projectNgPackagr = requireProjectModule( + getSystemPath(root), 'ng-packagr') as typeof ngPackagr; + const packageJsonPath = getSystemPath(resolve(root, normalize(options.project))); + + projectNgPackagr.ngPackagr() + .forProject(packageJsonPath) + .build() + .then(() => { + obs.next({ success: true }); + obs.complete(); + }) + .catch((e) => obs.error(e)); + }); + } + +} + +export default NgPackagrBuilder; diff --git a/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts b/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts new file mode 100644 index 0000000000..47e874528f --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/src/build/index_spec_large.ts @@ -0,0 +1,62 @@ +/** + * @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, TargetSpecifier } from '@angular-devkit/architect'; +import { experimental, join, normalize } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { concatMap, tap } from 'rxjs/operators'; + + +// TODO: replace this with an "it()" macro that's reusable globally. +let linuxOnlyIt: typeof it = it; +if (process.platform.startsWith('win')) { + linuxOnlyIt = xit; +} + + +describe('NgPackagr Builder', () => { + const workspaceFile = normalize('angular.json'); + const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any + const workspaceRoot = join(devkitRoot, + 'tests/@angular_devkit/build_ng_packagr/ng-packaged/'); + + // TODO: move TestProjectHost from build-angular to architect, or somewhere else, where it + // can be imported from. + const host = new NodeJsSyncHost(); + const workspace = new experimental.workspace.Workspace(workspaceRoot, host); + + it('works', (done) => { + const targetSpec: TargetSpecifier = { project: 'lib', target: 'build' }; + + return workspace.loadWorkspaceFromHost(workspaceFile).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + concatMap(arch => arch.run(arch.getBuilderConfiguration(targetSpec))), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + linuxOnlyIt('tests works', (done) => { + const targetSpec: TargetSpecifier = { project: 'lib', target: 'test' }; + + return workspace.loadWorkspaceFromHost(workspaceFile).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + concatMap(arch => arch.run(arch.getBuilderConfiguration(targetSpec))), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); + + it('lint works', (done) => { + const targetSpec: TargetSpecifier = { project: 'lib', target: 'lint' }; + + return workspace.loadWorkspaceFromHost(workspaceFile).pipe( + concatMap(ws => new Architect(ws).loadArchitect()), + concatMap(arch => arch.run(arch.getBuilderConfiguration(targetSpec))), + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).subscribe(undefined, done.fail, done); + }, 30000); +}); diff --git a/packages/angular_devkit/build_ng_packagr/src/build/schema.json b/packages/angular_devkit/build_ng_packagr/src/build/schema.json new file mode 100644 index 0000000000..7c784f38bd --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/src/build/schema.json @@ -0,0 +1,15 @@ +{ + "title": "ng-packagr Target", + "description": "ng-packagr target options for Build Architect.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The file path of the package.json for distribution via npm." + } + }, + "additionalProperties": false, + "required": [ + "project" + ] +} diff --git a/packages/angular_devkit/build_ng_packagr/src/index.ts b/packages/angular_devkit/build_ng_packagr/src/index.ts new file mode 100644 index 0000000000..51ed774b37 --- /dev/null +++ b/packages/angular_devkit/build_ng_packagr/src/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './build'; diff --git a/packages/angular_devkit/build_optimizer/BUILD b/packages/angular_devkit/build_optimizer/BUILD index c77a095030..054aef5ded 100644 --- a/packages/angular_devkit/build_optimizer/BUILD +++ b/packages/angular_devkit/build_optimizer/BUILD @@ -23,6 +23,7 @@ ts_library( "src/purify/**", "src/index.ts", "**/*_spec.ts", + "**/*_spec_large.ts", ], ), tsconfig = "//:tsconfig.json", diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts index 1a14e6bd5c..8cf66d88c8 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/build-optimizer.ts @@ -37,11 +37,14 @@ const whitelistedAngularModules = [ /[\\/]node_modules[\\/]@angular[\\/]cdk[\\/]/, ]; +// TODO: this code is very fragile and should be reworked. +// See: https://github.com/angular/devkit/issues/523 const es5AngularModules = [ // Angular 4 packaging format has .es5.js as the extension. /\.es5\.js$/, // Angular 4 // Angular 5 has esm5 folders. - /[\\/]node_modules[\\/]@angular[\\/][^\\/]+[\\/]esm5[\\/]/, + // Angular 6 has fesm5 folders. + /[\\/]node_modules[\\/]@angular[\\/][^\\/]+[\\/]f?esm5[\\/]/, // All Angular versions have UMD with es5. /\.umd\.js$/, ]; diff --git a/packages/angular_devkit/build_optimizer/src/build-optimizer/webpack-loader.ts b/packages/angular_devkit/build_optimizer/src/build-optimizer/webpack-loader.ts index 215e488de6..e05666f8b5 100644 --- a/packages/angular_devkit/build_optimizer/src/build-optimizer/webpack-loader.ts +++ b/packages/angular_devkit/build_optimizer/src/build-optimizer/webpack-loader.ts @@ -32,6 +32,9 @@ export default function buildOptimizerLoader inputFilePath, outputFilePath, emitSourceMap: options.sourceMap, + isSideEffectFree: this._module + && this._module.factoryMeta + && this._module.factoryMeta.sideEffectFree, }); if (boOutput.emitSkipped || boOutput.content === null) { diff --git a/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes.ts b/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes.ts index a5402a696b..678cab7a32 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes.ts @@ -27,7 +27,7 @@ export function testPrefixClasses(content: string) { exportVarSetter, multiLineComment, /\(/, multiLineComment, /\s*function \(_super\) {/, newLine, - /\w*__extends\(\w+, _super\);/, + /\w*\.?__extends\(\w+, _super\);/, ], ].map(arr => new RegExp(arr.map(x => x.source).join(''), 'm')); @@ -179,19 +179,24 @@ function isDownleveledClass(node: ts.Node): boolean { return false; } - if (functionStatements.length < 3) { + if (functionStatements.length < 3 || !ts.isExpressionStatement(firstStatement)) { return false; } - if (!ts.isExpressionStatement(firstStatement) - || !ts.isCallExpression(firstStatement.expression)) { + if (!ts.isCallExpression(firstStatement.expression)) { return false; } const extendCallExpression = firstStatement.expression; - if (!ts.isIdentifier(extendCallExpression.expression) - || !extendCallExpression.expression.text.endsWith(extendsHelperName)) { + let functionName; + if (ts.isIdentifier(extendCallExpression.expression)) { + functionName = extendCallExpression.expression.text; + } else if (ts.isPropertyAccessExpression(extendCallExpression.expression)) { + functionName = extendCallExpression.expression.name.text; + } + + if (!functionName || !functionName.endsWith(extendsHelperName)) { return false; } diff --git a/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes_spec.ts index a5766eb02b..6ded564497 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/prefix-classes_spec.ts @@ -218,6 +218,30 @@ describe('prefix-classes', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); + it('works with tslib namespace import', () => { + const input = tags.stripIndent` + var BufferSubscriber = /** @class */ (function (_super) { + tslib_1.__extends(BufferSubscriber, _super); + function BufferSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + return BufferSubscriber; + }(OuterSubscriber)); + `; + const output = tags.stripIndent` + var BufferSubscriber = /*@__PURE__*/ (function (_super) { + tslib_1.__extends(BufferSubscriber, _super); + function BufferSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + return BufferSubscriber; + }(OuterSubscriber)); + `; + + expect(testPrefixClasses(input)).toBeTruthy(); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + it('fixes the RxJS use case (issue #214)', () => { const input = ` var ExtendedClass = /*@__PURE__*/ (/*@__PURE__*/ function (_super) { diff --git a/packages/angular_devkit/build_webpack/package.json b/packages/angular_devkit/build_webpack/package.json deleted file mode 100644 index 7da1fe91ab..0000000000 --- a/packages/angular_devkit/build_webpack/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@angular-devkit/build-webpack", - "version": "0.0.0", - "description": "Angular Webpack Build Facade", - "main": "src/index.js", - "typings": "src/index.d.ts", - "builders": "builders.json", - "scripts": { - "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" - }, - "dependencies": { - "@angular-devkit/build-optimizer": "0.0.0", - "@angular-devkit/architect": "0.0.0", - "@angular-devkit/core": "0.0.0", - "@ngtools/webpack": "^1.10.0-rc.0", - "autoprefixer": "^7.2.3", - "chalk": "~2.2.2", - "circular-dependency-plugin": "^4.3.0", - "clean-css": "^4.1.9", - "common-tags": "^1.5.1", - "copy-webpack-plugin": "^4.2.3", - "css-loader": "^0.28.7", - "denodeify": "^1.2.1", - "exports-loader": "^0.6.4", - "extract-text-webpack-plugin": "^3.0.2", - "file-loader": "^1.1.5", - "glob": "^7.0.3", - "html-webpack-plugin": "^2.30.1", - "karma-source-map-support": "^1.2.0", - "less": "^2.7.3", - "less-loader": "^4.0.5", - "license-webpack-plugin": "^1.1.1", - "lodash": "^4.17.4", - "memory-fs": "^0.4.1", - "minimatch": "^3.0.4", - "node-sass": "^4.7.2", - "opn": "^5.1.0", - "portfinder": "^1.0.13", - "postcss-import": "^11.0.0", - "postcss-loader": "^2.0.10", - "postcss-url": "^7.3.0", - "raw-loader": "^0.5.1", - "request": "^2.83.0", - "rxjs": "^5.5.6", - "sass-loader": "^6.0.6", - "silent-error": "^1.1.0", - "source-map-loader": "^0.2.3", - "source-map-support": "^0.5.0", - "style-loader": "^0.19.1", - "stylus": "^0.54.5", - "stylus-loader": "^3.0.1", - "tree-kill": "^1.2.0", - "uglifyjs-webpack-plugin": "^1.1.6", - "url-loader": "^0.6.2", - "webpack": "^3.10.0", - "webpack-dev-middleware": "^1.12.2", - "webpack-dev-server": "^2.11.0", - "webpack-merge": "^4.1.1", - "webpack-sources": "^1.0.1", - "webpack-subresource-integrity": "^1.0.3" - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/build-options.ts deleted file mode 100644 index 210316cb82..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/build-options.ts +++ /dev/null @@ -1,51 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -export interface BuildOptions { - target?: string; - environment?: string; - outputPath: string; - aot?: boolean; - sourcemaps?: boolean; - evalSourcemaps?: boolean; - vendorChunk?: boolean; - commonChunk?: boolean; - baseHref?: string; - deployUrl?: string; - verbose?: boolean; - progress?: boolean; - i18nFile?: string; - i18nFormat?: string; - i18nOutFile?: string; - i18nOutFormat?: string; - locale?: string; - missingTranslation?: string; - extractCss?: boolean; - bundleDependencies?: 'none' | 'all'; - watch?: boolean; - outputHashing?: string; - poll?: number; - app?: string; - deleteOutputPath?: boolean; - preserveSymlinks?: boolean; - extractLicenses?: boolean; - showCircularDependencies?: boolean; - buildOptimizer?: boolean; - namedChunks?: boolean; - subresourceIntegrity?: boolean; - forceTsCommonjs?: boolean; - serviceWorker?: boolean; - skipAppShell?: boolean; -} - -export interface WebpackConfigOptions { - projectRoot: string; - buildOptions: T; - appConfig: any; - tsConfig: any; - supportES2015: boolean; -} - -export interface WebpackTestOptions extends BuildOptions { - codeCoverage?: boolean; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/browser.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/browser.ts deleted file mode 100644 index 1c37fc8dd7..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/browser.ts +++ /dev/null @@ -1,131 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as fs from 'fs'; -import * as webpack from 'webpack'; -import * as path from 'path'; -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); - -import { packageChunkSort } from '../../utilities/package-chunk-sort'; -import { findUp } from '../../utilities/find-up'; -import { BaseHrefWebpackPlugin } from '../../lib/base-href-webpack'; -import { extraEntryParser, lazyChunksFilter } from './utils'; -import { WebpackConfigOptions } from '../build-options'; - - -export function getBrowserConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; - - const appRoot = path.resolve(projectRoot, appConfig.root); - - let extraPlugins: any[] = []; - - // figure out which are the lazy loaded entry points - const lazyChunks = lazyChunksFilter([ - ...extraEntryParser(appConfig.scripts, appRoot, 'scripts'), - ...extraEntryParser(appConfig.styles, appRoot, 'styles') - ]); - - if (buildOptions.vendorChunk) { - // Separate modules from node_modules into a vendor chunk. - // const nodeModules = path.resolve(projectRoot, 'node_modules'); - const nodeModules = findUp('node_modules', projectRoot); - if (!nodeModules) { - throw new Error('Cannot locale node_modules directory.') - } - // Resolves all symlink to get the actual node modules folder. - const realNodeModules = fs.realpathSync(nodeModules); - // --aot puts the generated *.ngfactory.ts in src/$$_gendir/node_modules. - const genDirNodeModules = path.resolve(appRoot, '$$_gendir', 'node_modules'); - - extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - chunks: ['main'], - minChunks: (module: any) => { - return module.resource - && ( module.resource.startsWith(nodeModules) - || module.resource.startsWith(genDirNodeModules) - || module.resource.startsWith(realNodeModules)); - } - })); - } - - if (buildOptions.sourcemaps) { - // See https://webpack.js.org/configuration/devtool/ for sourcemap types. - if (buildOptions.evalSourcemaps && buildOptions.target === 'development') { - // Produce eval sourcemaps for development with serve, which are faster. - extraPlugins.push(new webpack.EvalSourceMapDevToolPlugin({ - moduleFilenameTemplate: '[resource-path]', - sourceRoot: 'webpack:///' - })); - } else { - // Produce full separate sourcemaps for production. - extraPlugins.push(new webpack.SourceMapDevToolPlugin({ - filename: '[file].map[query]', - moduleFilenameTemplate: '[resource-path]', - fallbackModuleFilenameTemplate: '[resource-path]?[hash]', - sourceRoot: 'webpack:///' - })); - } - } - - if (buildOptions.commonChunk) { - extraPlugins.push(new webpack.optimize.CommonsChunkPlugin({ - name: 'main', - async: 'common', - children: true, - minChunks: 2 - })); - } - - if (buildOptions.subresourceIntegrity) { - extraPlugins.push(new SubresourceIntegrityPlugin({ - hashFuncNames: ['sha384'] - })); - } - - return { - resolve: { - mainFields: [ - ...(wco.supportES2015 ? ['es2015'] : []), - 'browser', 'module', 'main' - ] - }, - output: { - crossOriginLoading: buildOptions.subresourceIntegrity ? 'anonymous' : false - }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.resolve(appRoot, appConfig.index), - filename: path.resolve(projectRoot, buildOptions.outputPath as any, appConfig.index), - chunksSortMode: packageChunkSort(appConfig), - excludeChunks: lazyChunks, - xhtml: true, - minify: buildOptions.target === 'production' ? { - caseSensitive: true, - collapseWhitespace: true, - keepClosingSlash: true - } : false - }), - new BaseHrefWebpackPlugin({ - baseHref: buildOptions.baseHref as any - }), - new webpack.optimize.CommonsChunkPlugin({ - minChunks: Infinity, - name: 'inline' - }) - ].concat(extraPlugins), - node: { - fs: 'empty', - global: true, - crypto: 'empty', - tls: 'empty', - net: 'empty', - process: true, - module: false, - clearImmediate: false, - setImmediate: false - } - }; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/common.ts deleted file mode 100644 index 5e05ebbd7a..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/common.ts +++ /dev/null @@ -1,241 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as webpack from 'webpack'; -import * as path from 'path'; -import * as CopyWebpackPlugin from 'copy-webpack-plugin'; -import { NamedLazyChunksWebpackPlugin } from '../../plugins/named-lazy-chunks-webpack-plugin'; -import { extraEntryParser, getOutputHashFormat, AssetPattern } from './utils'; -import { isDirectory } from '../../utilities/is-directory'; -import { requireProjectModule } from '../../utilities/require-project-module'; -import { WebpackConfigOptions } from '../build-options'; -import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin'; -import { findUp } from '../../utilities/find-up'; - -const ProgressPlugin = require('webpack/lib/ProgressPlugin'); -const CircularDependencyPlugin = require('circular-dependency-plugin'); -const SilentError = require('silent-error'); - -/** - * Enumerate loaders and their dependencies from this file to let the dependency validator - * know they are used. - * - * require('source-map-loader') - * require('raw-loader') - * require('url-loader') - * require('file-loader') - * require('@angular-devkit/build-optimizer') - */ - -export function getCommonConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; - - const appRoot = path.resolve(projectRoot, appConfig.root); - const nodeModules = findUp('node_modules', projectRoot); - if (!nodeModules) { - throw new Error('Cannot locale node_modules directory.') - } - - let extraPlugins: any[] = []; - let extraRules: any[] = []; - let entryPoints: { [key: string]: string[] } = {}; - - if (appConfig.main) { - entryPoints['main'] = [path.resolve(appRoot, appConfig.main)]; - } - - if (appConfig.polyfills) { - entryPoints['polyfills'] = [path.resolve(appRoot, appConfig.polyfills)]; - } - - // determine hashing format - const hashFormat = getOutputHashFormat(buildOptions.outputHashing as any); - - // process global scripts - if (appConfig.scripts.length > 0) { - const globalScripts = extraEntryParser(appConfig.scripts, appRoot, 'scripts'); - const globalScriptsByEntry = globalScripts - .reduce((prev: { entry: string, paths: string[], lazy: boolean }[], curr) => { - - let existingEntry = prev.find((el) => el.entry === curr.entry); - if (existingEntry) { - existingEntry.paths.push(curr.path as string); - // All entries have to be lazy for the bundle to be lazy. - (existingEntry as any).lazy = existingEntry.lazy && curr.lazy; - } else { - prev.push({ entry: curr.entry as string, paths: [curr.path as string], - lazy: curr.lazy as boolean }); - } - return prev; - }, []); - - - // Add a new asset for each entry. - globalScriptsByEntry.forEach((script) => { - // Lazy scripts don't get a hash, otherwise they can't be loaded by name. - const hash = script.lazy ? '' : hashFormat.script; - extraPlugins.push(new ScriptsWebpackPlugin({ - name: script.entry, - sourceMap: buildOptions.sourcemaps, - filename: `${script.entry}${hash}.bundle.js`, - scripts: script.paths, - basePath: projectRoot, - })); - }); - } - - // process asset entries - if (appConfig.assets) { - const copyWebpackPluginPatterns = appConfig.assets.map((asset: string | AssetPattern) => { - // Convert all string assets to object notation. - asset = typeof asset === 'string' ? { glob: asset } : asset; - // Add defaults. - // Input is always resolved relative to the appRoot. - asset.input = path.resolve(appRoot, asset.input || '').replace(/\\/g, '/'); - asset.output = asset.output || ''; - asset.glob = asset.glob || ''; - - // Prevent asset configurations from writing outside of the output path, except if the user - // specify a configuration flag. - // Also prevent writing outside the project path. That is not overridable. - const absoluteOutputPath = path.resolve(buildOptions.outputPath as string); - const absoluteAssetOutput = path.resolve(absoluteOutputPath, asset.output); - const outputRelativeOutput = path.relative(absoluteOutputPath, absoluteAssetOutput); - - if (outputRelativeOutput.startsWith('..') || path.isAbsolute(outputRelativeOutput)) { - - const projectRelativeOutput = path.relative(projectRoot, absoluteAssetOutput); - if (projectRelativeOutput.startsWith('..') || path.isAbsolute(projectRelativeOutput)) { - const message = 'An asset cannot be written to a location outside the project.'; - throw new SilentError(message); - } - - if (!asset.allowOutsideOutDir) { - const message = 'An asset cannot be written to a location outside of the output path. ' - + 'You can override this message by setting the `allowOutsideOutDir` ' - + 'property on the asset to true in the CLI configuration.'; - throw new SilentError(message); - } - } - - // Prevent asset configurations from reading files outside of the project. - const projectRelativeInput = path.relative(projectRoot, asset.input); - if (projectRelativeInput.startsWith('..') || path.isAbsolute(projectRelativeInput)) { - const message = 'An asset cannot be read from a location outside the project.'; - throw new SilentError(message); - } - - // Ensure trailing slash. - if (isDirectory(path.resolve(asset.input))) { - asset.input += '/'; - } - - // Convert dir patterns to globs. - if (isDirectory(path.resolve(asset.input, asset.glob))) { - asset.glob = asset.glob + '/**/*'; - } - - // Escape the input in case it has special charaters and use to make glob absolute - const escapedInput = asset.input - .replace(/[\\|\*|\?|\!|\(|\)|\[|\]|\{|\}]/g, (substring) => `\\${substring}`); - - return { - context: asset.input, - to: asset.output, - from: { - glob: path.resolve(escapedInput, asset.glob), - dot: true - } - }; - }); - const copyWebpackPluginOptions = { ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'] }; - - const copyWebpackPluginInstance = new CopyWebpackPlugin(copyWebpackPluginPatterns, - copyWebpackPluginOptions); - - // Save options so we can use them in eject. - (copyWebpackPluginInstance as any)['copyWebpackPluginPatterns'] = copyWebpackPluginPatterns; - (copyWebpackPluginInstance as any)['copyWebpackPluginOptions'] = copyWebpackPluginOptions; - - extraPlugins.push(copyWebpackPluginInstance); - } - - if (buildOptions.progress) { - extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose, colors: true })); - } - - if (buildOptions.showCircularDependencies) { - extraPlugins.push(new CircularDependencyPlugin({ - exclude: /(\\|\/)node_modules(\\|\/)/ - })); - } - - if (buildOptions.buildOptimizer) { - extraRules.push({ - test: /\.js$/, - use: [{ - loader: '@angular-devkit/build-optimizer/webpack-loader', - options: { sourceMap: buildOptions.sourcemaps } - }] - }); - } - - if (buildOptions.namedChunks) { - extraPlugins.push(new NamedLazyChunksWebpackPlugin()); - } - - // Load rxjs path aliases. - // https://github.com/ReactiveX/rxjs/blob/master/doc/lettable-operators.md#build-and-treeshaking - let alias = {}; - try { - const rxjsPathMappingImport = wco.supportES2015 - ? 'rxjs/_esm2015/path-mapping' - : 'rxjs/_esm5/path-mapping'; - const rxPaths = requireProjectModule(projectRoot, rxjsPathMappingImport); - alias = rxPaths(nodeModules); - } catch (e) { } - - return { - resolve: { - extensions: ['.ts', '.js'], - modules: ['node_modules', nodeModules], - symlinks: !buildOptions.preserveSymlinks, - alias - }, - resolveLoader: { - modules: [nodeModules, 'node_modules'] - }, - context: __dirname, - entry: entryPoints, - output: { - path: path.resolve(projectRoot, buildOptions.outputPath as string), - publicPath: buildOptions.deployUrl, - filename: `[name]${hashFormat.chunk}.bundle.js`, - chunkFilename: `[id]${hashFormat.chunk}.chunk.js` - }, - module: { - rules: [ - { test: /\.html$/, loader: 'raw-loader' }, - { - test: /\.(eot|svg|cur)$/, - loader: 'file-loader', - options: { - name: `[name]${hashFormat.file}.[ext]`, - limit: 10000 - } - }, - { - test: /\.(jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/, - loader: 'url-loader', - options: { - name: `[name]${hashFormat.file}.[ext]`, - limit: 10000 - } - } - ].concat(extraRules) - }, - plugins: [ - new webpack.NoEmitOnErrorsPlugin() - ].concat(extraPlugins) - }; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/development.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/development.ts deleted file mode 100644 index dd90b2dad8..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/development.ts +++ /dev/null @@ -1,12 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import { NamedModulesPlugin } from 'webpack'; - -import { WebpackConfigOptions } from '../build-options'; - -export function getDevConfig(_wco: WebpackConfigOptions) { - return { - plugins: [new NamedModulesPlugin()] - }; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/production.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/production.ts deleted file mode 100644 index 8faa0e903e..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/production.ts +++ /dev/null @@ -1,175 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as path from 'path'; -import * as webpack from 'webpack'; -import * as fs from 'fs'; -import * as semver from 'semver'; -import { stripIndent } from 'common-tags'; -import { LicenseWebpackPlugin } from 'license-webpack-plugin'; -import { PurifyPlugin } from '@angular-devkit/build-optimizer'; -import { BundleBudgetPlugin } from '../../plugins/bundle-budget'; -import { StaticAssetPlugin } from '../../plugins/static-asset'; -import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin'; -import { WebpackConfigOptions } from '../build-options'; -import { NEW_SW_VERSION } from '../../utilities/service-worker'; - -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); - -const OLD_SW_VERSION = '>= 1.0.0-beta.5 < 2.0.0'; - -/** - * license-webpack-plugin has a peer dependency on webpack-sources, list it in a comment to - * let the dependency validator know it is used. - * - * require('webpack-sources') - */ - - -export function getProdConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; - - let extraPlugins: any[] = []; - let entryPoints: { [key: string]: string[] } = {}; - - if (appConfig.serviceWorker) { - const nodeModules = path.resolve(projectRoot, 'node_modules'); - const swModule = path.resolve(nodeModules, '@angular/service-worker'); - - // @angular/service-worker is required to be installed when serviceWorker is true. - if (!fs.existsSync(swModule)) { - throw new Error(stripIndent` - Your project is configured with serviceWorker = true, but @angular/service-worker - is not installed. Run \`npm install --save-dev @angular/service-worker\` - and try again, or run \`ng set apps.0.serviceWorker=false\` in your .angular-cli.json. - `); - } - - // Read the version of @angular/service-worker and throw if it doesn't match the - // expected version. - const swPackageJson = fs.readFileSync(`${swModule}/package.json`).toString(); - const swVersion = JSON.parse(swPackageJson)['version']; - - const isLegacySw = semver.satisfies(swVersion, OLD_SW_VERSION); - const isModernSw = semver.gte(swVersion, NEW_SW_VERSION); - - if (!isLegacySw && !isModernSw) { - throw new Error(stripIndent` - The installed version of @angular/service-worker is ${swVersion}. This version of the CLI - requires the @angular/service-worker version to satisfy ${OLD_SW_VERSION}. Please upgrade - your service worker version. - `); - } - - if (isLegacySw) { - // Path to the worker script itself. - const workerPath = path.resolve(swModule, 'bundles/worker-basic.min.js'); - - // Path to a small script to register a service worker. - const registerPath = path.resolve(swModule, 'build/assets/register-basic.min.js'); - - // Sanity check - both of these files should be present in @angular/service-worker. - if (!fs.existsSync(workerPath) || !fs.existsSync(registerPath)) { - throw new Error(stripIndent` - The installed version of @angular/service-worker isn't supported by the CLI. - Please install a supported version. The following files should exist: - - ${registerPath} - - ${workerPath} - `); - } - - // CopyWebpackPlugin replaces GlobCopyWebpackPlugin, but AngularServiceWorkerPlugin depends - // on specific behaviour from latter. - // AngularServiceWorkerPlugin expects the ngsw-manifest.json to be present in the 'emit' phase - // but with CopyWebpackPlugin it's only there on 'after-emit'. - // So for now we keep it here, but if AngularServiceWorkerPlugin changes we remove it. - extraPlugins.push(new GlobCopyWebpackPlugin({ - patterns: [ - 'ngsw-manifest.json', - { glob: 'ngsw-manifest.json', - input: path.resolve(projectRoot, appConfig.root), output: '' } - ], - globOptions: { - cwd: projectRoot, - optional: true, - }, - })); - - // Load the Webpack plugin for manifest generation and install it. - const AngularServiceWorkerPlugin = require('@angular/service-worker/build/webpack') - .AngularServiceWorkerPlugin; - extraPlugins.push(new AngularServiceWorkerPlugin({ - baseHref: buildOptions.baseHref || '/', - })); - - // Copy the worker script into assets. - const workerContents = fs.readFileSync(workerPath).toString(); - extraPlugins.push(new StaticAssetPlugin('worker-basic.min.js', workerContents)); - - // Add a script to index.html that registers the service worker. - // TODO(alxhub): inline this script somehow. - entryPoints['sw-register'] = [registerPath]; - } - } - - extraPlugins.push(new BundleBudgetPlugin({ - budgets: appConfig.budgets - })); - - if (buildOptions.extractLicenses) { - extraPlugins.push(new LicenseWebpackPlugin({ - pattern: /^(MIT|ISC|BSD.*)$/, - suppressErrors: true, - perChunkOutput: false, - outputFilename: `3rdpartylicenses.txt` - })); - } - - const uglifyCompressOptions: any = { - // Disabled because of an issue with Mapbox GL when using the Webpack node global and UglifyJS: - // https://github.com/mapbox/mapbox-gl-js/issues/4359#issuecomment-303880888 - // https://github.com/angular/angular-cli/issues/5804 - // https://github.com/angular/angular-cli/pull/7931 - typeofs : false - }; - - if (buildOptions.buildOptimizer) { - // This plugin must be before webpack.optimize.UglifyJsPlugin. - extraPlugins.push(new PurifyPlugin()); - uglifyCompressOptions.pure_getters = true; - // PURE comments work best with 3 passes. - // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. - uglifyCompressOptions.passes = 3; - } - - return { - entry: entryPoints, - plugins: [ - new webpack.EnvironmentPlugin({ - 'NODE_ENV': 'production' - }), - new webpack.HashedModuleIdsPlugin(), - new webpack.optimize.ModuleConcatenationPlugin(), - ...extraPlugins, - // Uglify should be the last plugin as PurifyPlugin needs to be before it. - new UglifyJSPlugin({ - sourceMap: buildOptions.sourcemaps, - parallel: true, - uglifyOptions: { - ecma: wco.supportES2015 ? 6 : 5, - warnings: buildOptions.verbose, - ie8: false, - mangle: { - safari10: true, - }, - compress: uglifyCompressOptions, - output: { - ascii_only: true, - comments: false, - webkit: true, - }, - } - }), - ] - }; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/typescript.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/typescript.ts deleted file mode 100644 index bf65ade398..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/models/webpack-configs/typescript.ts +++ /dev/null @@ -1,168 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as path from 'path'; -import { stripIndent } from 'common-tags'; -import { - AotPlugin, - AotPluginOptions, - AngularCompilerPlugin, - AngularCompilerPluginOptions, - PLATFORM -} from '@ngtools/webpack'; -import { WebpackConfigOptions } from '../build-options'; - -const SilentError = require('silent-error'); - - -const g: any = global; -const webpackLoader: string = g['angularCliIsLocal'] - ? g.angularCliPackages['@ngtools/webpack'].main - : '@ngtools/webpack'; - - -function _createAotPlugin(wco: WebpackConfigOptions, options: any, useMain = true) { - const { appConfig, projectRoot, buildOptions } = wco; - options.compilerOptions = options.compilerOptions || {}; - - if (wco.buildOptions.preserveSymlinks) { - options.compilerOptions.preserveSymlinks = true; - } - - // Forcing commonjs seems to drastically improve rebuild speeds on webpack. - // Dev builds on watch mode will set this option to true. - if (wco.buildOptions.forceTsCommonjs) { - options.compilerOptions.module = 'commonjs'; - } - - // Read the environment, and set it in the compiler host. - let hostReplacementPaths: any = {}; - // process environment file replacement - if (appConfig.environments) { - if (!appConfig.environmentSource) { - let migrationMessage = ''; - if ('source' in appConfig.environments) { - migrationMessage = '\n\n' + stripIndent` - A new environmentSource entry replaces the previous source entry inside environments. - - To migrate angular-cli.json follow the example below: - - Before: - - "environments": { - "source": "environments/environment.ts", - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" - } - - - After: - - "environmentSource": "environments/environment.ts", - "environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" - } - `; - } - throw new SilentError( - `Environment configuration does not contain "environmentSource" entry.${migrationMessage}` - ); - - } - if (!(buildOptions.environment as any in appConfig.environments)) { - throw new SilentError(`Environment "${buildOptions.environment}" does not exist.`); - } - - const appRoot = path.resolve(projectRoot, appConfig.root); - const sourcePath = appConfig.environmentSource; - const envFile = appConfig.environments[buildOptions.environment as any]; - - hostReplacementPaths = { - [path.resolve(appRoot, sourcePath)]: path.resolve(appRoot, envFile) - }; - } - - if (AngularCompilerPlugin.isSupported()) { - const pluginOptions: AngularCompilerPluginOptions = Object.assign({}, { - mainPath: useMain ? path.join(projectRoot, appConfig.root, appConfig.main) : undefined, - i18nInFile: buildOptions.i18nFile, - i18nInFormat: buildOptions.i18nFormat, - i18nOutFile: buildOptions.i18nOutFile, - i18nOutFormat: buildOptions.i18nOutFormat, - locale: buildOptions.locale, - platform: appConfig.platform === 'server' ? PLATFORM.Server : PLATFORM.Browser, - missingTranslation: buildOptions.missingTranslation, - hostReplacementPaths, - sourceMap: buildOptions.sourcemaps, - }, options); - return new AngularCompilerPlugin(pluginOptions); - } else { - const pluginOptions: AotPluginOptions = Object.assign({}, { - mainPath: path.join(projectRoot, appConfig.root, appConfig.main), - i18nFile: buildOptions.i18nFile, - i18nFormat: buildOptions.i18nFormat, - locale: buildOptions.locale, - replaceExport: appConfig.platform === 'server', - missingTranslation: buildOptions.missingTranslation, - hostReplacementPaths, - sourceMap: buildOptions.sourcemaps, - // If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`. - exclude: [] - }, options); - return new AotPlugin(pluginOptions); - } -} - -export function getNonAotConfig(wco: WebpackConfigOptions) { - const { appConfig, projectRoot } = wco; - const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsConfig); - - return { - module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, - plugins: [ _createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }) ] - }; -} - -export function getAotConfig(wco: WebpackConfigOptions) { - const { projectRoot, buildOptions, appConfig } = wco; - const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsConfig); - - let pluginOptions: any = { tsConfigPath }; - - let boLoader: any = []; - if (buildOptions.buildOptimizer) { - boLoader = [{ - loader: '@angular-devkit/build-optimizer/webpack-loader', - options: { sourceMap: buildOptions.sourcemaps } - }]; - } - - const test = AngularCompilerPlugin.isSupported() - ? /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/ - : /\.ts$/; - - return { - module: { rules: [{ test, use: [...boLoader, webpackLoader] }] }, - plugins: [ _createAotPlugin(wco, pluginOptions) ] - }; -} - -export function getNonAotTestConfig(wco: WebpackConfigOptions) { - const { projectRoot, appConfig } = wco; - const tsConfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsConfig); - - let pluginOptions: any = { tsConfigPath, skipCodeGeneration: true }; - - if (appConfig.polyfills) { - // TODO: remove singleFileIncludes for 2.0, this is just to support old projects that did not - // include 'polyfills.ts' in `tsconfig.spec.json'. - const polyfillsPath = path.resolve(projectRoot, appConfig.root, appConfig.polyfills); - pluginOptions.singleFileIncludes = [polyfillsPath]; - } - - return { - module: { rules: [{ test: /\.ts$/, loader: webpackLoader }] }, - plugins: [ _createAotPlugin(wco, pluginOptions, false) ] - }; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/bundle-budget.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/bundle-budget.ts deleted file mode 100644 index 3ec03181da..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/bundle-budget.ts +++ /dev/null @@ -1,132 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -/** - * @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 { Budget, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; - -interface Thresholds { - maximumWarning?: number; - maximumError?: number; - minimumWarning?: number; - minimumError?: number; - warningLow?: number; - warningHigh?: number; - errorLow?: number; - errorHigh?: number; -} - -export interface BundleBudgetPluginOptions { - budgets: Budget[]; -} - -export class BundleBudgetPlugin { - constructor(private options: BundleBudgetPluginOptions) {} - - apply(compiler: any): void { - const { budgets } = this.options; - compiler.plugin('after-emit', (compilation: any, cb: Function) => { - if (!budgets || budgets.length === 0) { - cb(); - return; - } - - budgets.map(budget => { - const thresholds = this.calcualte(budget); - return { - budget, - thresholds, - sizes: calculateSizes(budget, compilation) - }; - }) - .forEach(budgetCheck => { - budgetCheck.sizes.forEach(size => { - if (budgetCheck.thresholds.maximumWarning) { - if (budgetCheck.thresholds.maximumWarning < size.size) { - compilation.warnings.push(`budgets, maximum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.maximumError) { - if (budgetCheck.thresholds.maximumError < size.size) { - compilation.errors.push(`budgets, maximum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.minimumWarning) { - if (budgetCheck.thresholds.minimumWarning > size.size) { - compilation.warnings.push(`budgets, minimum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.minimumError) { - if (budgetCheck.thresholds.minimumError > size.size) { - compilation.errors.push(`budgets, minimum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.warningLow) { - if (budgetCheck.thresholds.warningLow > size.size) { - compilation.warnings.push(`budgets, minimum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.warningHigh) { - if (budgetCheck.thresholds.warningHigh < size.size) { - compilation.warnings.push(`budgets, maximum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.errorLow) { - if (budgetCheck.thresholds.errorLow > size.size) { - compilation.errors.push(`budgets, minimum exceeded for ${size.label}.`); - } - } - if (budgetCheck.thresholds.errorHigh) { - if (budgetCheck.thresholds.errorHigh < size.size) { - compilation.errors.push(`budgets, maximum exceeded for ${size.label}.`); - } - } - }); - - }); - cb(); - }); - } - - private calcualte(budget: Budget): Thresholds { - let thresholds: Thresholds = {}; - if (budget.maximumWarning) { - thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 'pos'); - } - - if (budget.maximumError) { - thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 'pos'); - } - - if (budget.minimumWarning) { - thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, 'neg'); - } - - if (budget.minimumError) { - thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, 'neg'); - } - - if (budget.warning) { - thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, 'neg'); - } - - if (budget.warning) { - thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 'pos'); - } - - if (budget.error) { - thresholds.errorLow = calculateBytes(budget.error, budget.baseline, 'neg'); - } - - if (budget.error) { - thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 'pos'); - } - - return thresholds; - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts deleted file mode 100644 index bf5a34257c..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/cleancss-webpack-plugin.ts +++ /dev/null @@ -1,114 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -/** - * @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 { Compiler } from 'webpack'; -import { RawSource, SourceMapSource } from 'webpack-sources'; - -const CleanCSS = require('clean-css'); - -interface Chunk { - files: string[]; -} - -export interface CleanCssWebpackPluginOptions { - sourceMap: boolean; -} - -export class CleanCssWebpackPlugin { - - constructor(private options: Partial = {}) {} - - apply(compiler: Compiler): void { - compiler.plugin('compilation', (compilation: any) => { - compilation.plugin('optimize-chunk-assets', - (chunks: Array, callback: (err?: Error) => void) => { - - const cleancss = new CleanCSS({ - compatibility: 'ie9', - level: 2, - inline: false, - returnPromise: true, - sourceMap: this.options.sourceMap, - }); - - const files: string[] = [...compilation.additionalChunkAssets]; - - chunks.forEach(chunk => { - if (chunk.files && chunk.files.length > 0) { - files.push(...chunk.files); - } - }); - - const actions = files - .filter(file => file.endsWith('.css')) - .map(file => { - const asset = compilation.assets[file]; - if (!asset) { - return Promise.resolve(); - } - - let content: string; - let map: any; - if (asset.sourceAndMap) { - const sourceAndMap = asset.sourceAndMap(); - content = sourceAndMap.source; - map = sourceAndMap.map; - } else { - content = asset.source(); - } - - if (content.length === 0) { - return Promise.resolve(); - } - - return Promise.resolve() - .then(() => cleancss.minify(content, map)) - .then((output: any) => { - let hasWarnings = false; - if (output.warnings && output.warnings.length > 0) { - compilation.warnings.push(...output.warnings); - hasWarnings = true; - } - - if (output.errors && output.errors.length > 0) { - output.errors - .forEach((error: string) => compilation.errors.push(new Error(error))); - return; - } - - // generally means invalid syntax so bail - if (hasWarnings && output.stats.minifiedSize === 0) { - return; - } - - let newSource; - if (output.sourceMap) { - newSource = new SourceMapSource( - output.styles, - file, - output.sourceMap.toString(), - content, - map, - ); - } else { - newSource = new RawSource(output.styles); - } - - compilation.assets[file] = newSource; - }); - }); - - Promise.all(actions) - .then(() => callback()) - .catch(err => callback(err)); - }); - }); - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/glob-copy-webpack-plugin.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/glob-copy-webpack-plugin.ts deleted file mode 100644 index a1fa11460f..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/glob-copy-webpack-plugin.ts +++ /dev/null @@ -1,96 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as fs from 'fs'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as denodeify from 'denodeify'; -import { AssetPattern } from '../models/webpack-configs/utils'; -import { isDirectory } from '../utilities/is-directory'; - -const flattenDeep = require('lodash/flattenDeep'); -const globPromise = denodeify(glob); -const statPromise = denodeify(fs.stat); - -interface Asset { - originPath: string; - destinationPath: string; - relativePath: string; -} - -export interface GlobCopyWebpackPluginOptions { - patterns: (string | AssetPattern)[]; - globOptions: any; -} - -// Adds an asset to the compilation assets; -function addAsset(compilation: any, asset: Asset) { - const realPath = path.resolve(asset.originPath, asset.relativePath); - // Make sure that asset keys use forward slashes, otherwise webpack dev server - const servedPath = path.join(asset.destinationPath, asset.relativePath).replace(/\\/g, '/'); - - // Don't re-add existing assets. - if (compilation.assets[servedPath]) { - return Promise.resolve(); - } - - // Read file and add it to assets; - return statPromise(realPath) - .then((stat: any) => compilation.assets[servedPath] = { - size: () => stat.size, - source: () => fs.readFileSync(realPath) - }); -} - -export class GlobCopyWebpackPlugin { - constructor(private options: GlobCopyWebpackPluginOptions) { } - - apply(compiler: any): void { - let { patterns, globOptions } = this.options; - const defaultCwd = globOptions.cwd || compiler.options.context; - - // Force nodir option, since we can't add dirs to assets. - globOptions.nodir = true; - - // Process patterns. - patterns = patterns.map(pattern => { - // Convert all string patterns to Pattern type. - pattern = typeof pattern === 'string' ? { glob: pattern } : pattern; - // Add defaults - // Input is always resolved relative to the defaultCwd (appRoot) - pattern.input = path.resolve(defaultCwd, pattern.input || ''); - pattern.output = pattern.output || ''; - pattern.glob = pattern.glob || ''; - // Convert dir patterns to globs. - if (isDirectory(path.resolve(pattern.input, pattern.glob))) { - pattern.glob = pattern.glob + '/**/*'; - } - return pattern; - }); - - compiler.plugin('emit', (compilation: any, cb: any) => { - // Create an array of promises for each pattern glob - const globs = patterns.map((pattern: AssetPattern) => new Promise((resolve, reject) => - // Individual patterns can override cwd - globPromise(pattern.glob, Object.assign({}, globOptions, { cwd: pattern.input })) - // Map the results onto an Asset - .then((globResults: string[]) => globResults.map(res => ({ - originPath: pattern.input, - destinationPath: pattern.output, - relativePath: res - }))) - .then((asset: Asset) => resolve(asset)) - .catch(reject) - )); - - // Wait for all globs. - Promise.all(globs) - // Flatten results. - .then(assets => flattenDeep(assets)) - // Add each asset to the compilation. - .then(assets => - Promise.all(assets.map((asset: Asset) => addAsset(compilation, asset)))) - .then(() => cb()); - }); - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/named-lazy-chunks-webpack-plugin.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/named-lazy-chunks-webpack-plugin.ts deleted file mode 100644 index ffb118de50..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/named-lazy-chunks-webpack-plugin.ts +++ /dev/null @@ -1,54 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import * as webpack from 'webpack'; -import { basename } from 'path'; -const AsyncDependenciesBlock = require('webpack/lib/AsyncDependenciesBlock'); -const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); -const ImportDependency = require('webpack/lib/dependencies/ImportDependency'); - -// This just extends webpack.NamedChunksPlugin to prevent name collisions. -export class NamedLazyChunksWebpackPlugin extends webpack.NamedChunksPlugin { - constructor() { - // Append a dot and number if the name already exists. - const nameMap = new Map(); - function getUniqueName(baseName: string, request: string) { - let name = baseName; - let num = 0; - while (nameMap.has(name) && nameMap.get(name) !== request) { - name = `${baseName}.${num++}`; - } - nameMap.set(name, request); - return name; - } - - const nameResolver = (chunk: any) => { - // Entry chunks have a name already, use it. - if (chunk.name) { - return chunk.name; - } - - // Try to figure out if it's a lazy loaded route or import(). - if (chunk.blocks - && chunk.blocks.length > 0 - && chunk.blocks[0] instanceof AsyncDependenciesBlock - && chunk.blocks[0].dependencies.length === 1 - && (chunk.blocks[0].dependencies[0] instanceof ContextElementDependency - || chunk.blocks[0].dependencies[0] instanceof ImportDependency) - ) { - // Create chunkname from file request, stripping ngfactory and extension. - const request = chunk.blocks[0].dependencies[0].request; - const chunkName = basename(request).replace(/(\.ngfactory)?\.(js|ts)$/, ''); - if (!chunkName || chunkName === '') { - // Bail out if something went wrong with the name. - return null; - } - return getUniqueName(chunkName, request); - } - - return null; - }; - - super(nameResolver); - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/scripts-webpack-plugin.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/scripts-webpack-plugin.ts deleted file mode 100644 index 21be453fc0..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/scripts-webpack-plugin.ts +++ /dev/null @@ -1,143 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -/** - * @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 { Compiler, loader } from 'webpack'; -import { CachedSource, ConcatSource, OriginalSource, RawSource, Source } from 'webpack-sources'; -import { interpolateName } from 'loader-utils'; -import * as path from 'path'; - -const Chunk = require('webpack/lib/Chunk'); - -export interface ScriptsWebpackPluginOptions { - name: string; - sourceMap: boolean; - scripts: string[]; - filename: string; - basePath: string; -} - -interface ScriptOutput { - filename: string; - source: CachedSource; -} - -export class ScriptsWebpackPlugin { - private _lastBuildTime?: number; - private _cachedOutput?: ScriptOutput; - - constructor(private options: Partial = {}) {} - - shouldSkip(compilation: any, scripts: string[]): boolean { - if (this._lastBuildTime == undefined) { - this._lastBuildTime = Date.now(); - return false; - } - - for (let i = 0; i < scripts.length; i++) { - const scriptTime = compilation.fileTimestamps[scripts[i]]; - if (!scriptTime || scriptTime > this._lastBuildTime) { - this._lastBuildTime = Date.now(); - return false; - } - } - - return true; - } - - private _insertOutput(compilation: any, { filename, source }: ScriptOutput, cached = false) { - const chunk = new Chunk(); - chunk.rendered = !cached; - chunk.id = this.options.name; - chunk.ids = [chunk.id]; - chunk.name = this.options.name; - chunk.isInitial = () => true; - chunk.files.push(filename); - - compilation.chunks.push(chunk); - compilation.assets[filename] = source; - } - - apply(compiler: Compiler): void { - if (!this.options.scripts || this.options.scripts.length === 0) { - return; - } - - const scripts = this.options.scripts - .filter(script => !!script) - .map(script => path.resolve(this.options.basePath || '', script)); - - compiler.plugin('this-compilation', (compilation: any) => { - compilation.plugin('additional-assets', (callback: (err?: Error) => void) => { - if (this.shouldSkip(compilation, scripts)) { - if (this._cachedOutput) { - this._insertOutput(compilation, this._cachedOutput, true); - } - compilation.fileDependencies.push(...scripts); - - callback(); - - return; - } - - const sourceGetters = scripts.map(fullPath => { - return new Promise((resolve, reject) => { - compilation.inputFileSystem.readFile(fullPath, (err: Error, data: Buffer) => { - if (err) { - reject(err); - return; - } - - const content = data.toString(); - - let source; - if (this.options.sourceMap) { - // TODO: Look for source map file (for '.min' scripts, etc.) - - let adjustedPath = fullPath; - if (this.options.basePath) { - adjustedPath = path.relative(this.options.basePath, fullPath); - } - source = new OriginalSource(content, adjustedPath); - } else { - source = new RawSource(content); - } - - resolve(source); - }); - }); - }); - - Promise.all(sourceGetters) - .then(sources => { - const concatSource = new ConcatSource(); - sources.forEach(source => { - concatSource.add(source); - concatSource.add('\n;'); - }); - - const combinedSource = new CachedSource(concatSource); - const filename = interpolateName( - { resourcePath: 'scripts.js' } as loader.LoaderContext, - this.options.filename as any, - { content: combinedSource.source() }, - ); - - const output = { filename, source: combinedSource }; - this._insertOutput(compilation, output); - this._cachedOutput = output; - compilation.fileDependencies.push(...scripts); - - callback(); - }) - .catch((err: Error) => callback(err)); - }); - }); - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/static-asset.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/static-asset.ts deleted file mode 100644 index 76670e16ed..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/plugins/static-asset.ts +++ /dev/null @@ -1,17 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -export class StaticAssetPlugin { - - constructor(private name: string, private contents: string) {} - - apply(compiler: any): void { - compiler.plugin('emit', (compilation: any, cb: Function) => { - compilation.assets[this.name] = { - size: () => this.contents.length, - source: () => this.contents, - }; - cb(); - }); - } -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/package-chunk-sort.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/package-chunk-sort.ts deleted file mode 100644 index 3f53669bc8..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/package-chunk-sort.ts +++ /dev/null @@ -1,44 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import { ExtraEntry, extraEntryParser } from '../models/webpack-configs/utils'; - -// Sort chunks according to a predefined order: -// inline, polyfills, all styles, vendor, main -export function packageChunkSort(appConfig: any) { - let entryPoints = ['inline', 'polyfills', 'sw-register']; - - const pushExtraEntries = (extraEntry: ExtraEntry) => { - if (entryPoints.indexOf(extraEntry.entry as string) === -1) { - entryPoints.push(extraEntry.entry as string); - } - }; - - if (appConfig.styles) { - extraEntryParser(appConfig.styles, './', 'styles').forEach(pushExtraEntries); - } - - if (appConfig.scripts) { - extraEntryParser(appConfig.scripts, './', 'scripts').forEach(pushExtraEntries); - } - - entryPoints.push(...['vendor', 'main']); - - function sort(left: any, right: any) { - let leftIndex = entryPoints.indexOf(left.names[0]); - let rightindex = entryPoints.indexOf(right.names[0]); - - if (leftIndex > rightindex) { - return 1; - } else if (leftIndex < rightindex) { - return -1; - } else { - return 0; - } - } - - // We need to list of entry points for the Ejected webpack config to work (we reuse the function - // defined above). - (sort as any).entryPoints = entryPoints; - return sort; -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/require-project-module.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/require-project-module.ts deleted file mode 100644 index a6b068a294..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/require-project-module.ts +++ /dev/null @@ -1,9 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -const resolve = require('resolve'); - -// require dependencies within the target project -export function requireProjectModule(root: string, moduleName: string) { - return require(resolve.sync(moduleName, { basedir: root })); -} diff --git a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/service-worker/index.ts b/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/service-worker/index.ts deleted file mode 100644 index 95b375674b..0000000000 --- a/packages/angular_devkit/build_webpack/src/angular-cli-files/utilities/service-worker/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - -import { Filesystem } from '@angular/service-worker/config'; -import { oneLine } from 'common-tags'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as semver from 'semver'; - -export const NEW_SW_VERSION = '5.0.0-rc.0'; - -class CliFilesystem implements Filesystem { - constructor(private base: string) {} - - list(_path: string): Promise { - return Promise.resolve(this.syncList(_path)); - } - - private syncList(_path: string): string[] { - const dir = this.canonical(_path); - const entries = fs.readdirSync(dir).map( - (entry: string) => ({entry, stats: fs.statSync(path.posix.join(dir, entry))})); - const files = entries.filter((entry: any) => !entry.stats.isDirectory()) - .map((entry: any) => path.posix.join(_path, entry.entry)); - - return entries.filter((entry: any) => entry.stats.isDirectory()) - .map((entry: any) => path.posix.join(_path, entry.entry)) - .reduce((list: string[], subdir: string) => list.concat(this.syncList(subdir)), files); - } - - read(_path: string): Promise { - const file = this.canonical(_path); - return Promise.resolve(fs.readFileSync(file).toString()); - } - - hash(_path: string): Promise { - const sha1 = crypto.createHash('sha1'); - const file = this.canonical(_path); - const contents: Buffer = fs.readFileSync(file); - sha1.update(contents); - return Promise.resolve(sha1.digest('hex')); - } - - write(_path: string, contents: string): Promise { - const file = this.canonical(_path); - fs.writeFileSync(file, contents); - return Promise.resolve(); - } - - private canonical(_path: string): string { return path.posix.join(this.base, _path); } -} - -export function usesServiceWorker(projectRoot: string): boolean { - const nodeModules = path.resolve(projectRoot, 'node_modules'); - const swModule = path.resolve(nodeModules, '@angular/service-worker'); - if (!fs.existsSync(swModule)) { - return false; - } - - const swPackageJson = fs.readFileSync(`${swModule}/package.json`).toString(); - const swVersion = JSON.parse(swPackageJson)['version']; - - return semver.gte(swVersion, NEW_SW_VERSION); -} - -export function augmentAppWithServiceWorker(projectRoot: string, appRoot: string, - outputPath: string, baseHref: string): Promise { - const nodeModules = path.resolve(projectRoot, 'node_modules'); - const swModule = path.resolve(nodeModules, '@angular/service-worker'); - - // Path to the worker script itself. - const workerPath = path.resolve(swModule, 'ngsw-worker.js'); - const configPath = path.resolve(appRoot, 'ngsw-config.json'); - - if (!fs.existsSync(configPath)) { - throw new Error(oneLine`Error: Expected to find an ngsw-config.json configuration - file in the ${appRoot} folder. Either provide one or disable Service Worker - in .angular-cli.json.`); - } - const config = fs.readFileSync(configPath, 'utf8'); - - const Generator = require('@angular/service-worker/config').Generator; - const gen = new Generator(new CliFilesystem(outputPath), baseHref); - return gen - .process(JSON.parse(config)) - .then((output: Object) => { - const manifest = JSON.stringify(output, null, 2); - fs.writeFileSync(path.resolve(outputPath, 'ngsw.json'), manifest); - // Copy worker script to dist directory. - const workerCode = fs.readFileSync(workerPath); - fs.writeFileSync(path.resolve(outputPath, 'ngsw-worker.js'), workerCode); - }); -} diff --git a/packages/angular_devkit/build_webpack/src/browser/index.ts b/packages/angular_devkit/build_webpack/src/browser/index.ts deleted file mode 100644 index c3a5d2897f..0000000000 --- a/packages/angular_devkit/build_webpack/src/browser/index.ts +++ /dev/null @@ -1,258 +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 { BuildEvent, Builder, BuilderContext, Target } from '@angular-devkit/architect'; -import { getSystemPath } from '@angular-devkit/core'; -import { writeFileSync } from 'fs'; -import { resolve } from 'path'; -import { Observable } from 'rxjs/Observable'; -import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies -import * as webpack from 'webpack'; -import { - getAotConfig, - getBrowserConfig, - getCommonConfig, - getDevConfig, - getNonAotConfig, - getProdConfig, - getStylesConfig, -} from '../angular-cli-files/models/webpack-configs'; -import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils'; -import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; -import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; -import { - statsErrorsToString, - statsToString, - statsWarningsToString, -} from '../angular-cli-files/utilities/stats'; -const webpackMerge = require('webpack-merge'); - - -// TODO: Use quicktype to build our TypeScript interfaces from the JSON Schema itself, in -// the build system. -export interface BrowserBuilderOptions { - outputPath: string; - index: string; - main: string; - tsConfig: string; // previously 'tsconfig'. - aot: boolean; - vendorChunk: boolean; - commonChunk: boolean; - verbose: boolean; - progress: boolean; - extractCss: boolean; - bundleDependencies: 'none' | 'all'; - watch: boolean; - outputHashing: 'none' | 'all' | 'media' | 'bundles'; - deleteOutputPath: boolean; - preserveSymlinks: boolean; - extractLicenses: boolean; - showCircularDependencies: boolean; - buildOptimizer: boolean; - namedChunks: boolean; - subresourceIntegrity: boolean; - serviceWorker: boolean; - skipAppShell: boolean; - - // Options with no defaults. - // TODO: reconsider this list. - polyfills?: string; - baseHref?: string; - deployUrl?: string; - i18nFile?: string; - i18nFormat?: string; - i18nOutFile?: string; - i18nOutFormat?: string; - poll?: number; - - // A couple of options have different names. - sourceMap: boolean; // previously 'sourcemaps'. - evalSourceMap: boolean; // previously 'evalSourcemaps'. - optimizationLevel: number; // previously 'target'. - i18nLocale?: string; // previously 'locale'. - i18nMissingTranslation?: string; // previously 'missingTranslation'. - - // These options were not available as flags. - assets: AssetPattern[]; - scripts: ExtraEntryPoint[]; - styles: ExtraEntryPoint[]; - stylePreprocessorOptions: { includePaths: string[] }; - platform: 'browser' | 'server'; - - // Some options are not needed anymore. - // app?: string; // apps aren't used with build facade - - // TODO: figure out what to do about these. - environment?: string; // Maybe replace with 'fileReplacement' object? - forceTsCommonjs?: boolean; // Remove with webpack 4. - statsJson: boolean; -} - -export interface AssetPattern { - glob: string; - input: string; - output: string; - allowOutsideOutDir: boolean; -} - -export interface ExtraEntryPoint { - input: string; - output?: string; - lazy: boolean; -} - -export interface WebpackConfigOptions { - projectRoot: string; - buildOptions: BrowserBuilderOptions; - appConfig: BrowserBuilderOptions; - tsConfig: ts.ParsedCommandLine; - supportES2015: boolean; -} - -export class BrowserBuilder implements Builder { - - constructor(public context: BuilderContext) { } - - run(target: Target): Observable { - return new Observable(obs => { - const root = getSystemPath(target.root); - const options = target.options; - - // Ensure Build Optimizer is only used with AOT. - if (options.buildOptimizer && !options.aot) { - throw new Error('The `--build-optimizer` option cannot be used without `--aot`.'); - } - - if (options.deleteOutputPath) { - // TODO: do this in a webpack plugin https://github.com/johnagan/clean-webpack-plugin - // fs.removeSync(resolve(options.root, options.outputPath)); - } - - const webpackConfig = this.buildWebpackConfig(root, options); - const webpackCompiler = webpack(webpackConfig); - const statsConfig = getWebpackStatsConfig(options.verbose); - - const callback: webpack.compiler.CompilerCallback = (err, stats) => { - if (err) { - return obs.error(err); - } - - const json = stats.toJson('verbose'); - if (options.verbose) { - this.context.logger.info(stats.toString(statsConfig)); - } else { - this.context.logger.info(statsToString(json, statsConfig)); - } - - if (stats.hasWarnings()) { - this.context.logger.warn(statsWarningsToString(json, statsConfig)); - } - if (stats.hasErrors()) { - this.context.logger.error(statsErrorsToString(json, statsConfig)); - } - - // TODO: what should these events look like and contain? - obs.next({ success: true }); - - if (options.watch) { - return; - } else if (options.statsJson) { - writeFileSync( - resolve(root, options.outputPath, 'stats.json'), - JSON.stringify(stats.toJson(), null, 2), - ); - } - - if (stats.hasErrors()) { - obs.error(); - } else { - // if (!!app.serviceWorker && runTaskOptions.target === 'production' && - // usesServiceWorker(this.project.root) && runTaskOptions.serviceWorker !== false) { - // const appRoot = path.resolve(this.project.root, app.root); - // augmentAppWithServiceWorker(this.project.root, appRoot, path.resolve(outputPath), - // runTaskOptions.baseHref || '/') - // .then(() => resolve(), (err: any) => reject(err)); - // } else { - obs.complete(); - } - }; - - try { - // if (options.watch) { - // webpackCompiler.watch({ poll: options.poll }, callback); - // } else { - webpackCompiler.run(callback); - // } - } catch (err) { - if (err) { - this.context.logger.error( - '\nAn error occured during the build:\n' + ((err && err.stack) || err)); - } - throw err; - } - }); - } - - buildWebpackConfig(projectRoot: string, options: BrowserBuilderOptions) { - let wco: WebpackConfigOptions; - - // TODO: make target defaults into configurations instead - // options = this.addTargetDefaults(options); - - const tsconfigPath = resolve(projectRoot, options.tsConfig as string); - const tsConfig = readTsconfig(tsconfigPath); - - const projectTs = requireProjectModule(projectRoot, 'typescript') as typeof ts; - - const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 - && tsConfig.options.target !== projectTs.ScriptTarget.ES5; - - - // TODO: inside the configs, always use the project root and not the workspace root. - // Until then we have to pretend the app root is relative (``) but the same as `projectRoot`. - (options as any).root = ''; // tslint:disable-line:no-any - - wco = { - projectRoot, - // TODO: use only this.options, it contains all flags and configs items already. - buildOptions: options, - appConfig: options, - tsConfig, - supportES2015, - }; - - let targetConfig = {}; - switch (options.optimizationLevel) { - case 0: - targetConfig = getDevConfig(wco); - break; - case 1: - targetConfig = getProdConfig(wco); - break; - } - - const webpackConfigs: {}[] = [ - getCommonConfig(wco), - getBrowserConfig(wco), - getStylesConfig(wco), - // TODO: use project configurations for the --prod meta options. - targetConfig, - ]; - - if (wco.appConfig.main || wco.appConfig.polyfills) { - const typescriptConfigPartial = wco.buildOptions.aot - ? getAotConfig(wco) - : getNonAotConfig(wco); - webpackConfigs.push(typescriptConfigPartial); - } - - return webpackMerge(webpackConfigs); - } -} - -export default BrowserBuilder; diff --git a/packages/angular_devkit/build_webpack/src/browser/index_spec.ts b/packages/angular_devkit/build_webpack/src/browser/index_spec.ts deleted file mode 100644 index 617cc3279c..0000000000 --- a/packages/angular_devkit/build_webpack/src/browser/index_spec.ts +++ /dev/null @@ -1,93 +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, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { existsSync, readFileSync } from 'fs'; -import { relative, resolve } from 'path'; -import { concatMap, tap, toArray } from 'rxjs/operators'; - -const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any -const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); -const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); -const relativeBuilderPath = relative(root, builderPath); -const outputPath = resolve(root, 'dist'); -const host = new NodeJsSyncHost(); - -export const getWorkspace = (): Workspace => ({ - name: 'spec', - version: 1, - root: '', - defaultProject: 'app', - projects: { - app: { - root: 'src', - projectType: 'application', - defaultTarget: 'browser', - targets: { - browser: { - builder: `${relativeBuilderPath}:browser`, - options: { - outputPath: '../dist', - index: 'index.html', - main: 'main.ts', - polyfills: 'polyfills.ts', - tsConfig: 'tsconfig.app.json', - progress: false, - aot: false, - styles: [{ input: 'styles.css', lazy: false }], - scripts: [], - assets: [ - { glob: 'favicon.ico', input: './', output: './', allowOutsideOutDir: false }, - { glob: '**/*', input: 'assets', output: './', allowOutsideOutDir: false }, - ], - }, - }, - }, - }, - }, -}); - -describe('Browser Builder', () => { - it('builds targets', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - toArray(), - tap(events => expect(events.length).toBe(1)), - tap(() => { - expect(existsSync(resolve(outputPath, 'inline.bundle.js'))).toBe(true); - expect(existsSync(resolve(outputPath, 'main.bundle.js'))).toBe(true); - expect(existsSync(resolve(outputPath, 'polyfills.bundle.js'))).toBe(true); - expect(existsSync(resolve(outputPath, 'styles.bundle.js'))).toBe(true); - expect(existsSync(resolve(outputPath, 'vendor.bundle.js'))).toBe(true); - expect(existsSync(resolve(outputPath, 'favicon.ico'))).toBe(true); - expect(existsSync(resolve(outputPath, 'index.html'))).toBe(true); - }), - ).subscribe(done, done.fail); - }, 30000); - - it('builds aot target', (done) => { - const workspace = getWorkspace(); - workspace.projects.app.targets.browser.options.aot = true; - - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(workspace).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - toArray(), - tap(events => expect(events.length).toBe(1)), - tap(() => { - const mainBundlePath = resolve(outputPath, 'main.bundle.js'); - expect(existsSync(mainBundlePath)).toBe(true); - expect(readFileSync(mainBundlePath, 'utf-8')).toMatch( - /platformBrowser.*bootstrapModuleFactory.*AppModuleNgFactory/); - }), - ).subscribe(done, done.fail); - }, 30000); -}); diff --git a/packages/angular_devkit/build_webpack/src/dev-server/index_spec.ts b/packages/angular_devkit/build_webpack/src/dev-server/index_spec.ts deleted file mode 100644 index f285d0a2d1..0000000000 --- a/packages/angular_devkit/build_webpack/src/dev-server/index_spec.ts +++ /dev/null @@ -1,70 +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, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { IncomingMessage } from 'http'; -import { relative, resolve } from 'path'; -import * as _request from 'request'; -import { fromPromise } from 'rxjs/observable/fromPromise'; -import { concatMap, take, tap } from 'rxjs/operators'; -import { getWorkspace as getBrowserWorkspace } from '../browser/index_spec'; - - -const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any -const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); -const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); -const relativeBuilderPath = relative(root, builderPath); -const host = new NodeJsSyncHost(); - -export const getWorkspace = (): Workspace => { - const workspace = getBrowserWorkspace(); - workspace.projects.app.defaultTarget = 'devServer'; - workspace.projects.app.targets['devServer'] = { - builder: `${relativeBuilderPath}:devServer`, - options: { - browserTarget: 'app:browser', - watch: false, - }, - }; - - return workspace; -}; - -describe('Dev Server Target', () => { - it('runs', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - concatMap(() => fromPromise(request('http://localhost:4200/index.html'))), - tap(response => expect(response).toContain('HelloWorldApp')), - take(1), - ).subscribe(undefined, done.fail, done); - }, 30000); -}); - -export function request(url: string): Promise { - return new Promise((resolve, reject) => { - const options = { - url: url, - headers: { 'Accept': 'text/html' }, - agentOptions: { rejectUnauthorized: false }, - }; - // tslint:disable-next-line:no-any - _request(options, (error: any, response: IncomingMessage, body: string) => { - if (error) { - reject(error); - } else if (response.statusCode && response.statusCode >= 400) { - reject(new Error(`Requesting "${url}" returned status code ${response.statusCode}.`)); - } else { - resolve(body); - } - }); - }); -} diff --git a/packages/angular_devkit/build_webpack/src/extract-i18n/index_spec.ts b/packages/angular_devkit/build_webpack/src/extract-i18n/index_spec.ts deleted file mode 100644 index 2f69d70cf8..0000000000 --- a/packages/angular_devkit/build_webpack/src/extract-i18n/index_spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { existsSync, readFileSync, unlinkSync } from 'fs'; -import { relative, resolve } from 'path'; -import { concatMap, tap, toArray } from 'rxjs/operators'; -import { getWorkspace as getBrowserWorkspace } from '../browser/index_spec'; - - -describe('Extract i18n Target', () => { - const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any - const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); - const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); - const relativeBuilderPath = relative(root, builderPath); - const host = new NodeJsSyncHost(); - const extractionFile = resolve(root, 'src', 'messages.xlf'); - - const getWorkspace = (): Workspace => { - const workspace = getBrowserWorkspace(); - workspace.projects.app.defaultTarget = 'extractI18n'; - workspace.projects.app.targets['extractI18n'] = { - builder: `${relativeBuilderPath}:extractI18n`, - options: { - browserTarget: 'app:browser', - }, - }; - - return workspace; - }; - - beforeEach(() => { - if (existsSync(extractionFile)) { - unlinkSync(extractionFile); - } - }); - - it('builds targets', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - toArray(), - tap(events => expect(events.length).toBe(0)), - tap(() => { - expect(existsSync(extractionFile)).toBe(true); - expect(readFileSync(extractionFile)).toMatch(/i18n test/); - }), - ).subscribe(done, done.fail); - }, 30000); -}); diff --git a/packages/angular_devkit/build_webpack/src/karma/index.ts b/packages/angular_devkit/build_webpack/src/karma/index.ts deleted file mode 100644 index 5e04f7ebdd..0000000000 --- a/packages/angular_devkit/build_webpack/src/karma/index.ts +++ /dev/null @@ -1,150 +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 { BuildEvent, Builder, BuilderContext, Target } from '@angular-devkit/architect'; -import { getSystemPath } from '@angular-devkit/core'; -import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies -import { - getCommonConfig, - getDevConfig, - getNonAotTestConfig, - getStylesConfig, - getTestConfig, -} from '../angular-cli-files/models/webpack-configs'; -import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig'; -import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module'; -import { - AssetPattern, - ExtraEntryPoint, -} from '../browser'; -const webpackMerge = require('webpack-merge'); - - -export interface KarmaBuilderOptions { - main: string; - tsConfig: string; // previously 'tsconfig'. - karmaConfig: string; // previously 'config'. - watch: boolean; - codeCoverage: boolean; - progress: boolean; - preserveSymlinks?: boolean; - - // Options with no defaults. - polyfills?: string; - poll?: number; - port?: number; - browsers?: string; - - // A couple of options have different names. - sourceMap: boolean; // previously 'sourcemaps'. - - // These options were not available as flags. - assets: AssetPattern[]; - scripts: ExtraEntryPoint[]; - styles: ExtraEntryPoint[]; - stylePreprocessorOptions: { includePaths: string[] }; - - // Some options are not needed anymore. - // app?: string; // apps aren't used with build facade - // singleRun?: boolean; // same as watch - // colors: boolean; // we just passed it to the karma config - // logLevel?: string; // same as above - // reporters?: string; // same as above - - // TODO: figure out what to do about these. - environment?: string; // Maybe replace with 'fileReplacement' object? - forceTsCommonjs?: boolean; // Remove with webpack 4. -} - -export class KarmaBuilder implements Builder { - constructor(public context: BuilderContext) { } - - run(target: Target): Observable { - - const root = getSystemPath(target.root); - const options = target.options; - - return new Observable(obs => { - const karma = requireProjectModule(root, 'karma'); - const karmaConfig = path.resolve(root, options.karmaConfig); - - // TODO: adjust options to account for not passing them blindly to karma. - // const karmaOptions: any = Object.assign({}, options); - // tslint:disable-next-line:no-any - const karmaOptions: any = { - singleRun: !options.watch, - }; - - // Convert browsers from a string to an array - if (options.browsers) { - karmaOptions.browsers = options.browsers.split(','); - } - - karmaOptions.webpackBuildFacade = { - options: options, - webpackConfig: this._buildWebpackConfig(root, options), - }; - - // TODO: inside the configs, always use the project root and not the workspace root. - // Until then we pretend the app root is relative (``) but the same as `projectRoot`. - (karmaOptions.webpackBuildFacade.options as any).root = ''; // tslint:disable-line:no-any - - // Assign additional karmaConfig options to the local ngapp config - karmaOptions.configFile = karmaConfig; - - // :shipit: - const karmaServer = new karma.Server(karmaOptions, () => obs.complete()); - karmaServer.start(); - }); - } - - private _buildWebpackConfig(projectRoot: string, options: KarmaBuilderOptions) { - // tslint:disable-next-line:no-any - let wco: any; - - const tsconfigPath = path.resolve(projectRoot, options.tsConfig as string); - const tsConfig = readTsconfig(tsconfigPath); - - const projectTs = requireProjectModule(projectRoot, 'typescript') as typeof ts; - - const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 - && tsConfig.options.target !== projectTs.ScriptTarget.ES5; - - const compatOptions = { - ...options, - // TODO: inside the configs, always use the project root and not the workspace root. - // Until then we have to pretend the app root is relative (``) but the same as `projectRoot`. - root: '', - // Some asset logic inside getCommonConfig needs outputPath to be set. - outputPath: '', - }; - - wco = { - projectRoot, - // TODO: use only this.options, it contains all flags and configs items already. - buildOptions: compatOptions, - appConfig: compatOptions, - tsConfig, - supportES2015, - }; - - const webpackConfigs: {}[] = [ - getCommonConfig(wco), - getStylesConfig(wco), - getDevConfig(wco), - getNonAotTestConfig(wco), - getTestConfig(wco), - ]; - - return webpackMerge(webpackConfigs); - } -} - -export default KarmaBuilder; diff --git a/packages/angular_devkit/build_webpack/src/karma/index_spec.ts b/packages/angular_devkit/build_webpack/src/karma/index_spec.ts deleted file mode 100644 index d770259972..0000000000 --- a/packages/angular_devkit/build_webpack/src/karma/index_spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { relative, resolve } from 'path'; -import { concatMap, tap, toArray } from 'rxjs/operators'; - - -describe('Karma Target', () => { - const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any - const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); - const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); - const relativeBuilderPath = relative(root, builderPath); - const host = new NodeJsSyncHost(); - - const getWorkspace = (): Workspace => ({ - name: 'spec', - version: 1, - root: '', - defaultProject: 'app', - projects: { - app: { - root: 'src', - projectType: 'application', - defaultTarget: 'karma', - targets: { - karma: { - builder: `${relativeBuilderPath}:karma`, - options: { - main: 'test.ts', - polyfills: 'polyfills.ts', - // Use Chrome Headless for CI envs. - browsers: 'ChromeHeadless', - tsConfig: 'tsconfig.spec.json', - karmaConfig: '../karma.conf.js', - progress: false, - styles: [{ input: 'styles.css', lazy: false }], - scripts: [], - assets: [ - { glob: 'favicon.ico', input: './', output: './', allowOutsideOutDir: false }, - { glob: '**/*', input: 'assets', output: './', allowOutsideOutDir: false }, - ], - }, - }, - }, - }, - }, - }); - - it('runs', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - toArray(), - tap(events => expect(events.length).toBe(0)), - ).subscribe(done, done.fail); - }, 30000); -}); diff --git a/packages/angular_devkit/build_webpack/src/protractor/index_spec.ts b/packages/angular_devkit/build_webpack/src/protractor/index_spec.ts deleted file mode 100644 index c5e4ce86c8..0000000000 --- a/packages/angular_devkit/build_webpack/src/protractor/index_spec.ts +++ /dev/null @@ -1,45 +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, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { relative, resolve } from 'path'; -import { concatMap, take } from 'rxjs/operators'; -import { getWorkspace as getDevServerWorkspace } from '../dev-server/index_spec'; - - -describe('Protractor Target', () => { - const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any - const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); - const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); - const relativeBuilderPath = relative(root, builderPath); - const host = new NodeJsSyncHost(); - - const getWorkspace = (): Workspace => { - const workspace = getDevServerWorkspace(); - workspace.projects.app.defaultTarget = 'protractor'; - workspace.projects.app.targets['protractor'] = { - builder: `${relativeBuilderPath}:protractor`, - options: { - protractorConfig: '../protractor.conf.js', - devServerTarget: 'app:devServer', - }, - }; - - return workspace; - }; - - it('runs', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - take(1), - ).subscribe(done, done.fail); - }, 30000); -}); diff --git a/packages/angular_devkit/build_webpack/src/tslint/index_spec.ts b/packages/angular_devkit/build_webpack/src/tslint/index_spec.ts deleted file mode 100644 index bcb7f0d9a8..0000000000 --- a/packages/angular_devkit/build_webpack/src/tslint/index_spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect, Workspace } from '@angular-devkit/architect'; -import { normalize } from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { relative, resolve } from 'path'; -import { concatMap, tap, toArray } from 'rxjs/operators'; - - -describe('Tslint Target', () => { - const devkitRoot = (global as any)._DevKitRoot; // tslint:disable-line:no-any - const root = resolve(devkitRoot, 'tests/@angular_devkit/build_webpack/hello-world-app/'); - const builderPath = resolve(devkitRoot, 'packages/angular_devkit/build_webpack'); - const relativeBuilderPath = relative(root, builderPath); - const host = new NodeJsSyncHost(); - - const getWorkspace = (): Workspace => ({ - name: 'spec', - version: 1, - root: '', - defaultProject: 'app', - projects: { - app: { - root: 'src', - projectType: 'application', - defaultTarget: 'tslint', - targets: { - tslint: { - builder: `${relativeBuilderPath}:tslint`, - options: { - tslintConfig: '../tslint.json', - tsConfig: 'tsconfig.app.json', - exclude: ['**/node_modules/**'], - }, - }, - }, - }, - }, - }); - - it('runs', (done) => { - const architect = new Architect(normalize(root), host); - architect.loadWorkspaceFromJson(getWorkspace()).pipe( - concatMap((architect) => architect.run(architect.getTarget())), - toArray(), - tap(events => expect(events.length).toBe(0)), - ).subscribe(done, done.fail); - }, 30000); -}); diff --git a/packages/angular_devkit/core/BUILD b/packages/angular_devkit/core/BUILD index 54118427a7..ab34a37643 100644 --- a/packages/angular_devkit/core/BUILD +++ b/packages/angular_devkit/core/BUILD @@ -14,6 +14,7 @@ ts_library( include = ["src/**/*.ts"], exclude = [ "src/**/*_spec.ts", + "src/**/*_spec_large.ts", "src/**/*_benchmark.ts", ], ), @@ -30,6 +31,7 @@ ts_library( include = ["node/**/*.ts"], exclude = [ "node/**/*_spec.ts", + "node/**/*_spec_large.ts", "tools/**/*_benchmark.ts", ], ), @@ -44,7 +46,10 @@ ts_library( ts_library( name = "spec", srcs = glob( - include = ["**/*_spec.ts"], + include = [ + "**/*_spec.ts", + "**/*_spec_large.ts", + ], ), deps = [ ":core", diff --git a/packages/angular_devkit/core/node/host.ts b/packages/angular_devkit/core/node/host.ts index 8c6ff0d864..7cdcf6ca6f 100644 --- a/packages/angular_devkit/core/node/host.ts +++ b/packages/angular_devkit/core/node/host.ts @@ -6,17 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ import * as fs from 'fs'; -import { Observable } from 'rxjs/Observable'; -import { empty } from 'rxjs/observable/empty'; -import { from as observableFrom } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; -import { concat } from 'rxjs/operators/concat'; -import { concatMap } from 'rxjs/operators/concatMap'; -import { ignoreElements } from 'rxjs/operators/ignoreElements'; -import { map } from 'rxjs/operators/map'; -import { mergeMap } from 'rxjs/operators/mergeMap'; -import { publish } from 'rxjs/operators/publish'; -import { refCount } from 'rxjs/operators/refCount'; +import { EMPTY, Observable, concat, from as observableFrom } from 'rxjs'; +import { + concatMap, + ignoreElements, + map, + mergeMap, + publish, + refCount, +} from 'rxjs/operators'; import { Path, PathFragment, @@ -123,15 +121,16 @@ export class NodeJsAsyncHost implements virtualFs.Host { }; _recurseList(path); - return observableFrom(allFiles) - .pipe( + return concat( + observableFrom(allFiles).pipe( mergeMap(p => _callFs(fs.unlink, getSystemPath(p))), ignoreElements(), - concat(observableFrom(allDirs).pipe( - concatMap(p => _callFs(fs.rmdir, getSystemPath(p))), - )), + ), + observableFrom(allDirs).pipe( + concatMap(p => _callFs(fs.rmdir, getSystemPath(p))), map(() => {}), - ); + ), + ); } else { return _callFs(fs.unlink, getSystemPath(path)); } @@ -170,8 +169,8 @@ export class NodeJsAsyncHost implements virtualFs.Host { ); } - // Some hosts may not support stats. - stats(path: Path): Observable> | null { + // Some hosts may not support stat. + stat(path: Path): Observable> | null { return _callFs(fs.stat, getSystemPath(path)); } @@ -224,24 +223,30 @@ export class NodeJsSyncHost implements virtualFs.Host { } write(path: Path, content: virtualFs.FileBuffer): Observable { - // Create folders if necessary. - const _createDir = (path: Path) => { - if (fs.existsSync(getSystemPath(path))) { - return; - } + return new Observable(obs => { + // Create folders if necessary. + const _createDir = (path: Path) => { + if (fs.existsSync(getSystemPath(path))) { + return; + } + _createDir(dirname(path)); + fs.mkdirSync(getSystemPath(path)); + }; _createDir(dirname(path)); - fs.mkdirSync(getSystemPath(path)); - }; - _createDir(dirname(path)); - fs.writeFileSync(getSystemPath(path), new Uint8Array(content)); + fs.writeFileSync(getSystemPath(path), new Uint8Array(content)); - return empty(); + obs.next(); + obs.complete(); + }); } read(path: Path): Observable { - const buffer = fs.readFileSync(getSystemPath(path)); + return new Observable(obs => { + const buffer = fs.readFileSync(getSystemPath(path)); - return observableOf(new Uint8Array(buffer).buffer as virtualFs.FileBuffer); + obs.next(new Uint8Array(buffer).buffer as virtualFs.FileBuffer); + obs.complete(); + }); } delete(path: Path): Observable { @@ -257,39 +262,49 @@ export class NodeJsSyncHost implements virtualFs.Host { fs.unlinkSync(getSystemPath(path)); } - return empty(); + return EMPTY; }), ); } rename(from: Path, to: Path): Observable { - fs.renameSync(getSystemPath(from), getSystemPath(to)); - - return empty(); + return new Observable(obs => { + fs.renameSync(getSystemPath(from), getSystemPath(to)); + obs.next(); + obs.complete(); + }); } list(path: Path): Observable { - return observableOf(fs.readdirSync(getSystemPath(path))).pipe( - map(names => names.map(name => fragment(name))), - ); + return new Observable(obs => { + const names = fs.readdirSync(getSystemPath(path)); + obs.next(names.map(name => fragment(name))); + obs.complete(); + }); } exists(path: Path): Observable { - return observableOf(fs.existsSync(getSystemPath(path))); + return new Observable(obs => { + obs.next(fs.existsSync(getSystemPath(path))); + obs.complete(); + }); } isDirectory(path: Path): Observable { // tslint:disable-next-line:non-null-operator - return this.stats(path) !.pipe(map(stat => stat.isDirectory())); + return this.stat(path) !.pipe(map(stat => stat.isDirectory())); } isFile(path: Path): Observable { // tslint:disable-next-line:non-null-operator - return this.stats(path) !.pipe(map(stat => stat.isFile())); + return this.stat(path) !.pipe(map(stat => stat.isFile())); } - // Some hosts may not support stats. - stats(path: Path): Observable> | null { - return observableOf(fs.statSync(getSystemPath(path))); + // Some hosts may not support stat. + stat(path: Path): Observable> { + return new Observable(obs => { + obs.next(fs.statSync(getSystemPath(path))); + obs.complete(); + }); } // Some hosts may not support watching. diff --git a/packages/angular_devkit/core/node/host_spec.ts b/packages/angular_devkit/core/node/host_spec.ts index 4b98b3d062..7be99bd388 100644 --- a/packages/angular_devkit/core/node/host_spec.ts +++ b/packages/angular_devkit/core/node/host_spec.ts @@ -11,12 +11,18 @@ import { normalize, virtualFs } from '@angular-devkit/core'; import { NodeJsAsyncHost, NodeJsSyncHost } from '@angular-devkit/core/node'; import * as fs from 'fs'; -import { Observable } from 'rxjs/Observable'; -import { Subscription } from 'rxjs/Subscription'; +import { Observable, Subscription } from 'rxjs'; const temp = require('temp'); +// TODO: replace this with an "it()" macro that's reusable globally. +let linuxOnlyIt: typeof it = it; +if (process.platform.startsWith('win') || process.platform.startsWith('darwin')) { + linuxOnlyIt = xit; +} + + describe('NodeJsAsyncHost', () => { let root: string; let host: virtualFs.Host; @@ -30,7 +36,7 @@ describe('NodeJsAsyncHost', () => { .subscribe({ complete() { done(); } }); }); - it('can watch', done => { + linuxOnlyIt('can watch', done => { let obs: Observable; let subscription: Subscription; const content = virtualFs.stringToFileBuffer('hello world'); @@ -57,7 +63,7 @@ describe('NodeJsAsyncHost', () => { subscription.unsubscribe(); }) .then(done, done.fail); - }, 10000000); + }, 30000); }); @@ -74,7 +80,7 @@ describe('NodeJsSyncHost', () => { host.delete(normalize('/')); }); - it('can watch', done => { + linuxOnlyIt('can watch', done => { let obs: Observable; let subscription: Subscription; const content = virtualFs.stringToFileBuffer('hello world'); @@ -103,6 +109,6 @@ describe('NodeJsSyncHost', () => { subscription.unsubscribe(); }) .then(done, done.fail); - }, 10000000); + }, 30000); }); diff --git a/packages/angular_devkit/core/node/resolve.ts b/packages/angular_devkit/core/node/resolve.ts index 2846863bd1..26618a0dda 100644 --- a/packages/angular_devkit/core/node/resolve.ts +++ b/packages/angular_devkit/core/node/resolve.ts @@ -35,7 +35,7 @@ function _caller(): string[] { const stack = (new Error()).stack as {}[] | undefined as { getFileName(): string }[] | undefined; error.prepareStackTrace = origPrepareStackTrace; - return stack ? stack.map(x => x.getFileName()) : []; + return stack ? stack.map(x => x.getFileName()).filter(x => !!x) : []; } diff --git a/packages/angular_devkit/core/node/testing/index.ts b/packages/angular_devkit/core/node/testing/index.ts new file mode 100644 index 0000000000..ec9d6fd9e2 --- /dev/null +++ b/packages/angular_devkit/core/node/testing/index.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Path, PathFragment, getSystemPath, join, normalize, virtualFs } from '../../src'; +import { NodeJsSyncHost } from '../host'; + +/** + * A Sync Scoped Host that creates a temporary directory and scope to it. + */ +export class TempScopedNodeJsSyncHost extends virtualFs.ScopedHost { + protected _sync: virtualFs.SyncDelegateHost; + protected _root: Path; + + constructor() { + const root = normalize(path.join(os.tmpdir(), `devkit-host-${+Date.now()}-${process.pid}`)); + fs.mkdirSync(getSystemPath(root)); + + super(new NodeJsSyncHost(), root); + this._root = root; + } + + get files(): Path[] { + const sync = this.sync; + function _visit(p: Path): Path[] { + return sync.list(p) + .map((fragment: PathFragment) => join(p, fragment)) + .reduce((files: Path[], path: PathFragment) => { + if (sync.isDirectory(path)) { + return files.concat(_visit(path)); + } else { + return files.concat(path); + } + }, [] as Path[]); + } + + return _visit(normalize('/')); + } + + get root() { return this._root; } + get sync() { + if (!this._sync) { + this._sync = new virtualFs.SyncDelegateHost(this); + } + + return this._sync; + } +} diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index 25407cd3f9..c1c3589339 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -14,6 +14,6 @@ "ajv": "~5.5.1", "chokidar": "^1.7.0", "source-map": "^0.5.6", - "rxjs": "^5.5.6" + "rxjs": "^6.0.0-beta.3" } } diff --git a/packages/angular_devkit/core/src/experimental.ts b/packages/angular_devkit/core/src/experimental.ts new file mode 100644 index 0000000000..9e2f0199cf --- /dev/null +++ b/packages/angular_devkit/core/src/experimental.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as workspace from './workspace'; + +export { workspace }; diff --git a/packages/angular_devkit/core/src/index.ts b/packages/angular_devkit/core/src/index.ts index d64907491b..b8125bcd0c 100644 --- a/packages/angular_devkit/core/src/index.ts +++ b/packages/angular_devkit/core/src/index.ts @@ -5,6 +5,7 @@ * Use 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 experimental from './experimental'; import * as logging from './logger'; import * as terminal from './terminal'; @@ -14,6 +15,7 @@ export * from './utils'; export * from './virtual-fs'; export { + experimental, logging, terminal, }; diff --git a/packages/angular_devkit/core/src/json/schema/index.ts b/packages/angular_devkit/core/src/json/schema/index.ts index 01c8e6e949..862c127a6b 100644 --- a/packages/angular_devkit/core/src/json/schema/index.ts +++ b/packages/angular_devkit/core/src/json/schema/index.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ export * from './interface'; +export * from './pointer'; export * from './registry'; export * from './visitor'; diff --git a/packages/angular_devkit/core/src/json/schema/interface.ts b/packages/angular_devkit/core/src/json/schema/interface.ts index 774552bef3..41aff9d094 100644 --- a/packages/angular_devkit/core/src/json/schema/interface.ts +++ b/packages/angular_devkit/core/src/json/schema/interface.ts @@ -5,14 +5,56 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { JsonValue } from '..'; +import { Observable } from 'rxjs'; +import { JsonArray, JsonObject, JsonValue } from '..'; +export type JsonPointer = string & { + __PRIVATE_DEVKIT_JSON_POINTER: void; +}; + +export type SchemaValidatorError = + RefValidatorError | + LimitValidatorError | + AdditionalPropertiesValidatorError | + FormatValidatorError | + RequiredValidatorError; + +export interface SchemaValidatorErrorBase { + keyword: string; + dataPath: string; + message?: string; + data?: JsonValue; +} + +export interface RefValidatorError extends SchemaValidatorErrorBase { + keyword: '$ref'; + params: { ref: string }; +} + +export interface LimitValidatorError extends SchemaValidatorErrorBase { + keyword: 'maxItems' | 'minItems' | 'maxLength' | 'minLength' | 'maxProperties' | 'minProperties'; + params: { limit: number }; +} + +export interface AdditionalPropertiesValidatorError extends SchemaValidatorErrorBase { + keyword: 'additionalProperties'; + params: { additionalProperty: string }; +} + +export interface FormatValidatorError extends SchemaValidatorErrorBase { + keyword: 'format'; + params: { format: string }; +} + +export interface RequiredValidatorError extends SchemaValidatorErrorBase { + keyword: 'required'; + params: { missingProperty: string }; +} export interface SchemaValidatorResult { data: JsonValue; success: boolean; - errors?: string[]; + errors?: SchemaValidatorError[]; } export interface SchemaValidator { @@ -31,7 +73,25 @@ export interface SchemaFormat { formatter: SchemaFormatter; } +export interface SmartDefaultProvider { + (schema: JsonObject): T | Observable; +} + +export interface SchemaKeywordValidator { + ( + // tslint:disable-next-line:no-any + data: JsonValue, + schema: JsonValue, + parent: JsonObject | JsonArray | undefined, + parentProperty: string | number | undefined, + pointer: JsonPointer, + // tslint:disable-next-line:no-any + rootData: JsonValue, + ): boolean | Observable; +} + export interface SchemaRegistry { compile(schema: Object): Observable; addFormat(format: SchemaFormat): void; + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; } diff --git a/packages/angular_devkit/core/src/json/schema/pointer.ts b/packages/angular_devkit/core/src/json/schema/pointer.ts new file mode 100644 index 0000000000..39d0541698 --- /dev/null +++ b/packages/angular_devkit/core/src/json/schema/pointer.ts @@ -0,0 +1,31 @@ +/** + * @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 { JsonPointer } from './interface'; + + +export function buildJsonPointer(fragments: string[]): JsonPointer { + return ( + '/' + fragments.map(f => { + return f.replace(/~/g, '~0') + .replace(/\//g, '~1'); + }).join('/') + ) as JsonPointer; +} +export function joinJsonPointer(root: JsonPointer, ...others: string[]): JsonPointer { + if (root == '/') { + return buildJsonPointer(others); + } + + return (root + buildJsonPointer(others)) as JsonPointer; +} +export function parseJsonPointer(pointer: JsonPointer): string[] { + if (pointer === '') { return []; } + if (pointer.charAt(0) !== '/') { throw new Error('Relative pointer: ' + pointer); } + + return pointer.substring(1).split(/\//).map(str => str.replace(/~1/g, '/').replace(/~0/g, '~')); +} diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index cd9a56407c..4f59b2a010 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -7,30 +7,78 @@ */ import * as ajv from 'ajv'; import * as http from 'http'; -import { Observable } from 'rxjs/Observable'; -import { fromPromise } from 'rxjs/observable/fromPromise'; -import { of as observableOf } from 'rxjs/observable/of'; -import { concatMap, switchMap } from 'rxjs/operators'; -import { map } from 'rxjs/operators/map'; -import { PartiallyOrderedSet } from '../../utils'; +import { Observable, from, of as observableOf } from 'rxjs'; +import { concatMap, map, switchMap, tap } from 'rxjs/operators'; +import { BaseException } from '../../exception/exception'; +import { PartiallyOrderedSet, isObservable } from '../../utils'; import { JsonObject, JsonValue } from '../interface'; import { SchemaFormat, SchemaFormatter, SchemaRegistry, SchemaValidator, + SchemaValidatorError, SchemaValidatorResult, + SmartDefaultProvider, } from './interface'; import { addUndefinedDefaults } from './transforms'; import { JsonVisitor, visitJson } from './visitor'; +// This interface should be exported from ajv, but they only export the class and not the type. +interface AjvValidationError { + message: string; + errors: Array; + ajv: true; + validation: true; +} + +export class SchemaValidationException extends BaseException { + public readonly errors: SchemaValidatorError[]; + + constructor( + errors?: SchemaValidatorError[], + baseMessage = 'Schema validation failed with the following errors:', + ) { + if (!errors || errors.length === 0) { + super('Schema validation failed.'); + + return; + } + + const messages = SchemaValidationException.createMessages(errors); + super(`${baseMessage}\n ${messages.join('\n ')}`); + this.errors = errors; + } + + public static createMessages(errors?: SchemaValidatorError[]): string[] { + if (!errors || errors.length === 0) { + return []; + } + + const messages = errors.map((err) => { + let message = `Data path ${JSON.stringify(err.dataPath)} ${err.message}`; + if (err.keyword === 'additionalProperties') { + message += `(${err.params.additionalProperty})`; + } + + return message + '.'; + }); + + return messages; + } +} + export class CoreSchemaRegistry implements SchemaRegistry { private _ajv: ajv.Ajv; private _uriCache = new Map(); private _pre = new PartiallyOrderedSet(); private _post = new PartiallyOrderedSet(); + private _smartDefaultKeyword = false; + private _sourceMap = new Map>(); + private _smartDefaultRecord = new Map(); + constructor(formats: SchemaFormat[] = []) { /** * Build an AJV instance that will be used to validate schemas. @@ -136,7 +184,10 @@ export class CoreSchemaRegistry implements SchemaRegistry { // in synchronous (if available). let validator: Observable; try { - const maybeFnValidate = this._ajv.compile(schema); + const maybeFnValidate = this._ajv.compile({ + $async: this._smartDefaultKeyword ? true : undefined, + ...schema, + }); validator = observableOf(maybeFnValidate); } catch (e) { // Propagate the error. @@ -157,44 +208,36 @@ export class CoreSchemaRegistry implements SchemaRegistry { return validator .pipe( - // tslint:disable-next-line:no-any - map(validate => (data: any): Observable => { - let dataObs = observableOf(data); - this._pre.forEach(visitor => - dataObs = dataObs.pipe( - concatMap(data => { - return visitJson(data as JsonValue, visitor, schema, this._resolver, validate); - }), - ), - ); - - return dataObs.pipe( + map(validate => (data: JsonValue): Observable => { + return observableOf(data).pipe( + ...[...this._pre].map(visitor => concatMap((data: JsonValue) => { + return visitJson(data, visitor, schema, this._resolver, validate); + })), + ).pipe( switchMap(updatedData => { const result = validate(updatedData); return typeof result == 'boolean' ? observableOf([updatedData, result]) - : fromPromise((result as PromiseLike) - .then(result => [updatedData, result])); + : from((result as Promise) + .then(r => [updatedData, true]) + .catch((err: Error | AjvValidationError) => { + if ((err as AjvValidationError).ajv) { + validate.errors = (err as AjvValidationError).errors; + + return Promise.resolve([updatedData, false]); + } + + return Promise.reject(err); + })); }), switchMap(([data, valid]) => { if (valid) { - let dataObs = observableOf(data); - this._post.forEach(visitor => - dataObs = dataObs.pipe( - concatMap(data => { - return visitJson( - data as JsonValue, - visitor, - schema, - this._resolver, - validate, - ); - }), - ), - ); - - return dataObs.pipe( + return this._applySmartDefaults(data).pipe( + ...[...this._post].map(visitor => concatMap(data => { + return visitJson(data as JsonValue, visitor, schema, this._resolver, validate); + })), + ).pipe( map(data => [data, valid]), ); } else { @@ -203,23 +246,13 @@ export class CoreSchemaRegistry implements SchemaRegistry { }), map(([data, valid]) => { if (valid) { - // tslint:disable-next-line:no-any - const schemaDataMap = new WeakMap(); - schemaDataMap.set(schema, data); - return { data, success: true } as SchemaValidatorResult; } return { data, success: false, - errors: (validate.errors || []) - .map((err) => `Data path ${JSON.stringify(err.dataPath)} ${err.message}${ - err.keyword === 'additionalProperties' && err.params - // tslint:disable-next-line:no-any - ? ` (${(err.params as any)['additionalProperty']}).` - : '.' - }`), + errors: (validate.errors || []), } as SchemaValidatorResult; }), ); @@ -232,7 +265,11 @@ export class CoreSchemaRegistry implements SchemaRegistry { const validate = (data: any) => { const result = format.formatter.validate(data); - return result instanceof Observable ? result.toPromise() : result; + if (typeof result == 'boolean') { + return result; + } else { + return result.toPromise(); + } }; this._ajv.addFormat(format.name, { @@ -242,4 +279,126 @@ export class CoreSchemaRegistry implements SchemaRegistry { // tslint:disable-next-line:no-any } as any); } + + addSmartDefaultProvider(source: string, provider: SmartDefaultProvider) { + if (this._sourceMap.has(source)) { + throw new Error(source); + } + + this._sourceMap.set(source, provider); + + if (!this._smartDefaultKeyword) { + this._smartDefaultKeyword = true; + + this._ajv.addKeyword('$default', { + modifying: true, + async: true, + compile: (schema, _parentSchema, it) => { + const source = this._sourceMap.get((schema as JsonObject).$source as string); + + if (!source) { + throw new Error(`Invalid source: ${JSON.stringify(source)}.`); + } + + // We cheat, heavily. + this._smartDefaultRecord.set( + // tslint:disable-next-line:no-any + JSON.stringify((it as any).dataPathArr.slice(1, (it as any).dataLevel + 1) as string[]), + schema, + ); + + return function() { + return > Promise.resolve(true); + }; + }, + }); + } + } + + // tslint:disable-next-line:no-any + private _applySmartDefaults(data: any): Observable { + function _set( + // tslint:disable-next-line:no-any + data: any, + fragments: string[], + value: {}, + // tslint:disable-next-line:no-any + parent: any | null = null, + parentProperty?: string, + ): void { + for (let i = 0; i < fragments.length; i++) { + const f = fragments[i]; + + if (f[0] == 'i') { + if (!Array.isArray(data)) { + return; + } + + for (let j = 0; j < data.length; j++) { + _set(data[j], fragments.slice(i + 1), value, data, '' + j); + } + + return; + } else if (f.startsWith('key')) { + if (typeof data !== 'object') { + return; + } + + Object.getOwnPropertyNames(data).forEach(property => { + _set(data[property], fragments.slice(i + 1), value, data, property); + }); + + return; + } else if (f.startsWith('\'') && f[f.length - 1] == '\'') { + const property = f + .slice(1, -1) + .replace(/\\'/g, '\'') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\f/g, '\f') + .replace(/\\t/g, '\t'); + + // We know we need an object because the fragment is a property key. + if (!data && parent !== null && parentProperty) { + data = parent[parentProperty] = {}; + } + parent = data; + parentProperty = property; + + data = data[property]; + } else { + return; + } + } + + if (parent && parentProperty && parent[parentProperty] === undefined) { + parent[parentProperty] = value; + } + } + + return observableOf(data).pipe( + ...[...this._smartDefaultRecord.entries()].map(([pointer, schema]) => { + return concatMap(data => { + const fragments = JSON.parse(pointer); + const source = this._sourceMap.get((schema as JsonObject).$source as string); + + if (!source) { + throw new Error('Invalid source.'); + } + + let value = source(schema); + if (!isObservable(value)) { + value = observableOf(value); + } + + return (value as Observable<{}>).pipe( + // Synchronously set the new data at the proper JsonSchema path. + tap(x => _set(data, fragments, x)), + // But return the data object. + map(() => data), + ); + }); + }), + ); + } } diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index df3240e264..5ea66b8ef5 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -7,7 +7,7 @@ */ // tslint:disable:no-any // tslint:disable:non-null-operator -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { CoreSchemaRegistry } from './registry'; @@ -121,13 +121,37 @@ describe('CoreSchemaRegistry', () => { mergeMap(validator => validator(data)), map(result => { expect(result.success).toBe(false); - expect(result.errors).toContain( - 'Data path "" should NOT have additional properties (notNum).'); + expect(result.errors && result.errors[0].message).toContain( + 'should NOT have additional properties'); }), ) .subscribe(done, done.fail); }); + it('fails on invalid additionalProperties async', done => { + const registry = new CoreSchemaRegistry(); + const data = { notNum: 'foo' }; + + registry + .compile({ + $async: true, + properties: { + num: { type: 'number' }, + }, + additionalProperties: false, + }).pipe( + mergeMap(validator => validator(data)), + map(result => { + expect(result.success).toBe(false); + expect(result.errors && result.errors[0].message).toContain( + 'should NOT have additional properties'); + expect(result.errors && result.errors[0].keyword).toBe('additionalProperties'); + }), + ) + .subscribe(done, done.fail); + }); + + // Synchronous failure is only used internally. // If it's meant to be used externally then this test should change to truly be synchronous // (i.e. not relyign on the observable). @@ -245,8 +269,100 @@ describe('CoreSchemaRegistry', () => { mergeMap(validator => validator(data)), map(result => { expect(result.success).toBe(false); - expect(result.errors && result.errors[0]).toBe( - 'Data path ".banana" should match format "is-hotdog".'); + expect(result.errors && result.errors[0]).toBeTruthy(); + expect(result.errors && result.errors[0].keyword).toBe('format'); + expect(result.errors && result.errors[0].dataPath).toBe('.banana'); + expect(result.errors && (result.errors[0].params as any).format).toBe('is-hotdog'); + }), + ) + .subscribe(done, done.fail); + }); + + it('supports smart defaults', done => { + const registry = new CoreSchemaRegistry(); + const data: any = { + arr: [{}], + }; + + registry.addSmartDefaultProvider('test', (schema) => { + expect(schema).toEqual({ + $source: 'test', + }); + + return true; + }); + registry.addSmartDefaultProvider('test2', (schema) => { + expect(schema).toEqual({ + $source: 'test2', + blue: 'yep', + }); + + return schema['blue']; + }); + registry.addSmartDefaultProvider('test3', (schema) => { + return [ 1, 2, 3 ]; + }); + + registry + .compile({ + properties: { + bool: { + $ref: '#/definitions/example', + }, + arr: { + items: { + properties: { + 'test': { + $ref: '#/definitions/other', + }, + }, + }, + }, + arr2: { + $ref: '#/definitions/test3', + }, + obj: { + properties: { + deep: { + properties: { + arr: { + $ref: '#/definitions/test3', + }, + }, + }, + }, + }, + }, + definitions: { + example: { + type: 'boolean', + $default: { + $source: 'test', + }, + }, + other: { + type: 'string', + $default: { + $source: 'test2', + blue: 'yep', + }, + }, + test3: { + type: 'array', + $default: { + $source: 'test3', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + map(result => { + expect(result.success).toBe(true); + expect(data.bool).toBe(true); + expect(data.arr[0].test).toBe('yep'); + expect(data.arr2).toEqual([1, 2, 3]); + expect(data.obj.deep.arr).toEqual([1, 2, 3]); }), ) .subscribe(done, done.fail); diff --git a/packages/angular_devkit/core/src/json/schema/transforms.ts b/packages/angular_devkit/core/src/json/schema/transforms.ts index df0e656563..31aead4070 100644 --- a/packages/angular_devkit/core/src/json/schema/transforms.ts +++ b/packages/angular_devkit/core/src/json/schema/transforms.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { JsonArray, JsonObject, JsonValue } from '..'; -import { JsonPointer } from './visitor'; +import { JsonPointer } from './interface'; export function addUndefinedDefaults( @@ -14,7 +14,7 @@ export function addUndefinedDefaults( _pointer: JsonPointer, schema?: JsonObject, _root?: JsonObject | JsonArray, -): JsonValue | undefined { +): JsonValue { if (value === undefined && schema) { if (schema.items || schema.type == 'array') { return []; @@ -38,5 +38,5 @@ export function addUndefinedDefaults( } } - return value; + return value as JsonValue; } diff --git a/packages/angular_devkit/core/src/json/schema/visitor.ts b/packages/angular_devkit/core/src/json/schema/visitor.ts index 17299e9191..b898548d18 100644 --- a/packages/angular_devkit/core/src/json/schema/visitor.ts +++ b/packages/angular_devkit/core/src/json/schema/visitor.ts @@ -5,16 +5,12 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { from } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; -import { concat, concatMap, ignoreElements, mergeMap, tap } from 'rxjs/operators'; -import { observable } from 'rxjs/symbol/observable'; +import { Observable, concat, from, of as observableOf } from 'rxjs'; +import { concatMap, ignoreElements, mergeMap, tap } from 'rxjs/operators'; import { JsonArray, JsonObject, JsonValue } from '..'; - -export type JsonPointer = string & { - __PRIVATE_DEVKIT_JSON_POINTER: void; -}; +import { isObservable } from '../../utils'; +import { JsonPointer } from './interface'; +import { buildJsonPointer, joinJsonPointer } from './pointer'; export interface JsonSchemaVisitor { ( @@ -27,11 +23,11 @@ export interface JsonSchemaVisitor { export interface JsonVisitor { ( - value: JsonValue | undefined, + value: JsonValue, pointer: JsonPointer, schema?: JsonObject, root?: JsonObject | JsonArray, - ): Observable | JsonValue | undefined; + ): Observable | JsonValue; } @@ -39,29 +35,6 @@ export interface ReferenceResolver { (ref: string, context?: ContextT): { context?: ContextT, schema?: JsonObject }; } - -export function buildJsonPointer(fragments: string[]): JsonPointer { - return ( - '/' + fragments.map(f => { - return f.replace(/~/g, '~0') - .replace(/\//g, '~1'); - }).join('/') - ) as JsonPointer; -} -export function joinJsonPointer(root: JsonPointer, ...others: string[]): JsonPointer { - if (root == '/') { - return buildJsonPointer(others); - } - - return (root + buildJsonPointer(others)) as JsonPointer; -} -export function parseJsonPointer(pointer: JsonPointer): string[] { - if (pointer === '') { return []; } - if (pointer.charAt(0) !== '/') { throw new Error('Relative pointer: ' + pointer); } - - return pointer.substring(1).split(/\//).map(str => str.replace(/~1/g, '/').replace(/~0/g, '~')); -} - function _getObjectSubSchema( schema: JsonObject | undefined, key: string, @@ -99,7 +72,7 @@ function _visitJsonRecursive( refResolver?: ReferenceResolver, context?: ContextT, // tslint:disable-line:no-any root?: JsonObject | JsonArray, -): Observable { +): Observable { if (schema && schema.hasOwnProperty('$ref') && typeof schema['$ref'] == 'string') { if (refResolver) { const resolved = refResolver(schema['$ref'] as string, context); @@ -110,43 +83,46 @@ function _visitJsonRecursive( const value = visitor(json, ptr, schema, root); - return ( - (typeof value == 'object' && value != null && observable in value) - ? value as Observable - : observableOf(value as JsonValue | undefined) + return (isObservable(value) + ? value as Observable + : observableOf(value as JsonValue) ).pipe( - concatMap((value: JsonValue | undefined) => { + concatMap((value: JsonValue) => { if (Array.isArray(value)) { - return from(value).pipe( - mergeMap((item, i) => { - return _visitJsonRecursive( - item, - visitor, - joinJsonPointer(ptr, '' + i), - _getObjectSubSchema(schema, '' + i), - refResolver, - context, - root || value, - ).pipe(tap(x => value[i] = x)); - }), - ignoreElements(), - concat(observableOf(value)), + return concat( + from(value).pipe( + mergeMap((item, i) => { + return _visitJsonRecursive( + item, + visitor, + joinJsonPointer(ptr, '' + i), + _getObjectSubSchema(schema, '' + i), + refResolver, + context, + root || value, + ).pipe(tap(x => value[i] = x)); + }), + ignoreElements(), + ), + observableOf(value), ); } else if (typeof value == 'object' && value !== null) { - return from(Object.getOwnPropertyNames(value)).pipe( - mergeMap(key => { - return _visitJsonRecursive( - value[key], - visitor, - joinJsonPointer(ptr, key), - _getObjectSubSchema(schema, key), - refResolver, - context, - root || value, - ).pipe(tap(x => value[key] = x)); - }), - ignoreElements(), - concat(observableOf(value)), + return concat( + from(Object.getOwnPropertyNames(value)).pipe( + mergeMap(key => { + return _visitJsonRecursive( + value[key], + visitor, + joinJsonPointer(ptr, key), + _getObjectSubSchema(schema, key), + refResolver, + context, + root || value, + ).pipe(tap(x => value[key] = x)); + }), + ignoreElements(), + ), + observableOf(value), ); } else { return observableOf(value); @@ -176,7 +152,7 @@ export function visitJson( schema?: JsonObject, refResolver?: ReferenceResolver, context?: ContextT, // tslint:disable-line:no-any -): Observable { +): Observable { return _visitJsonRecursive(json, visitor, buildJsonPointer([]), schema, refResolver, context); } diff --git a/packages/angular_devkit/core/src/json/schema/visitor_spec.ts b/packages/angular_devkit/core/src/json/schema/visitor_spec.ts index 3d6f10cbc8..ffe9389100 100644 --- a/packages/angular_devkit/core/src/json/schema/visitor_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/visitor_spec.ts @@ -5,8 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { from } from 'rxjs/observable/from'; +import { Observable, from } from 'rxjs'; import { JsonObject, JsonValue } from '..'; import { visitJson } from './visitor'; diff --git a/packages/angular_devkit/core/src/logger/indent.ts b/packages/angular_devkit/core/src/logger/indent.ts index e6fe06b686..7eb1958fc8 100644 --- a/packages/angular_devkit/core/src/logger/indent.ts +++ b/packages/angular_devkit/core/src/logger/indent.ts @@ -27,7 +27,7 @@ export class IndentLogger extends Logger { const indentMap = indentationMap[indentation]; this._observable = this._observable.pipe(map(entry => { - const l = entry.path.length; + const l = entry.path.filter(x => !!x).length; if (l >= indentMap.length) { let current = indentMap[indentMap.length - 1]; while (l >= indentMap.length) { diff --git a/packages/angular_devkit/core/src/logger/index.ts b/packages/angular_devkit/core/src/logger/index.ts index 50fafbe700..63bc0860b0 100644 --- a/packages/angular_devkit/core/src/logger/index.ts +++ b/packages/angular_devkit/core/src/logger/index.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ export * from './indent'; +export * from './level'; export * from './logger'; export * from './null-logger'; export * from './transform-logger'; diff --git a/packages/angular_devkit/core/src/logger/level.ts b/packages/angular_devkit/core/src/logger/level.ts new file mode 100644 index 0000000000..7de3255d90 --- /dev/null +++ b/packages/angular_devkit/core/src/logger/level.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '../json/interface'; +import { LogLevel, Logger } from './logger'; + +export class LevelTransformLogger extends Logger { + constructor( + public readonly name: string, + public readonly parent: Logger | null = null, + public readonly levelTransform: (level: LogLevel) => LogLevel, + ) { + super(name, parent); + } + + log(level: LogLevel, message: string, metadata: JsonObject = {}): void { + return super.log(this.levelTransform(level), message, metadata); + } + + createChild(name: string): Logger { + return new LevelTransformLogger(name, this, this.levelTransform); + } +} + +export class LevelCapLogger extends LevelTransformLogger { + static levelMap: {[cap: string]: {[level: string]: string}} = { + debug: { debug: 'debug', info: 'debug', warn: 'debug', error: 'debug', fatal: 'debug' }, + info: { debug: 'debug', info: 'info', warn: 'info', error: 'info', fatal: 'info' }, + warn: { debug: 'debug', info: 'info', warn: 'warn', error: 'warn', fatal: 'warn' }, + error: { debug: 'debug', info: 'info', warn: 'warn', error: 'error', fatal: 'error' }, + fatal: { debug: 'debug', info: 'info', warn: 'warn', error: 'error', fatal: 'fatal' }, + }; + + constructor( + public readonly name: string, + public readonly parent: Logger | null = null, + public readonly levelCap: LogLevel, + ) { + super(name, parent, (level: LogLevel) => { + return (LevelCapLogger.levelMap[levelCap][level] || level) as LogLevel; + }); + } +} diff --git a/packages/angular_devkit/core/src/logger/logger.ts b/packages/angular_devkit/core/src/logger/logger.ts index d8da9da8f1..e31cc7ce89 100644 --- a/packages/angular_devkit/core/src/logger/logger.ts +++ b/packages/angular_devkit/core/src/logger/logger.ts @@ -5,11 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { PartialObserver } from 'rxjs/Observer'; -import { Operator } from 'rxjs/Operator'; -import { Subject } from 'rxjs/Subject'; -import { Subscription } from 'rxjs/Subscription'; +import { Observable, Operator, PartialObserver, Subject, Subscription } from 'rxjs'; import { JsonObject } from '../json/interface'; @@ -77,7 +73,7 @@ export class Logger extends Observable implements LoggerApi { } this._metadata = { name, path }; this._observable = this._subject.asObservable(); - if (this.parent) { + if (this.parent && this.parent._subject) { // When the parent completes, complete us as well. this.parent._subject.subscribe(undefined, undefined, () => this.complete()); } @@ -98,7 +94,7 @@ export class Logger extends Observable implements LoggerApi { } createChild(name: string) { - return new Logger(name, this); + return new (this.constructor as typeof Logger)(name, this); } complete() { diff --git a/packages/angular_devkit/core/src/logger/null-logger.ts b/packages/angular_devkit/core/src/logger/null-logger.ts index 8136303019..4a687840a1 100644 --- a/packages/angular_devkit/core/src/logger/null-logger.ts +++ b/packages/angular_devkit/core/src/logger/null-logger.ts @@ -5,14 +5,14 @@ * Use 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 { empty } from 'rxjs/observable/empty'; +import { EMPTY } from 'rxjs'; import { Logger, LoggerApi } from './logger'; export class NullLogger extends Logger { constructor(parent: Logger | null = null) { super('', parent); - this._observable = empty(); + this._observable = EMPTY; } asApi(): LoggerApi { diff --git a/packages/angular_devkit/core/src/logger/transform-logger.ts b/packages/angular_devkit/core/src/logger/transform-logger.ts index 14fd29605d..4d024fd2ff 100644 --- a/packages/angular_devkit/core/src/logger/transform-logger.ts +++ b/packages/angular_devkit/core/src/logger/transform-logger.ts @@ -5,7 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { LogEntry, Logger } from './logger'; diff --git a/packages/angular_devkit/core/src/utils/array.ts b/packages/angular_devkit/core/src/utils/array.ts new file mode 100644 index 0000000000..e1969c1757 --- /dev/null +++ b/packages/angular_devkit/core/src/utils/array.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export function clean(array: Array): Array { + return array.filter(x => x !== undefined) as Array; +} diff --git a/packages/angular_devkit/core/src/utils/index.ts b/packages/angular_devkit/core/src/utils/index.ts index 4aa9bb9769..10b1855602 100644 --- a/packages/angular_devkit/core/src/utils/index.ts +++ b/packages/angular_devkit/core/src/utils/index.ts @@ -8,9 +8,11 @@ import * as tags from './literals'; import * as strings from './strings'; +export * from './array'; export * from './object'; export * from './template'; export * from './partially-ordered-set'; export * from './priority-queue'; +export * from './lang'; export { tags, strings }; diff --git a/packages/angular_devkit/core/src/utils/lang.ts b/packages/angular_devkit/core/src/utils/lang.ts new file mode 100644 index 0000000000..1fc0b09cbe --- /dev/null +++ b/packages/angular_devkit/core/src/utils/lang.ts @@ -0,0 +1,35 @@ +/** + * @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 + */ +// Borrowed from @angular/core +import { Observable } from 'rxjs'; + +/** + * Determine if the argument is shaped like a Promise + */ +// tslint:disable-next-line:no-any +export function isPromise(obj: any): obj is Promise { + // allow any Promise/A+ compliant thenable. + // It's up to the caller to ensure that obj.then conforms to the spec + return !!obj && typeof obj.then === 'function'; +} + +/** + * Determine if the argument is an Observable + */ +// tslint:disable-next-line:no-any +export function isObservable(obj: any | Observable): obj is Observable { + if (!obj || typeof obj !== 'object') { + return false; + } + + if (Symbol.observable && Symbol.observable in obj) { + return true; + } + + return typeof obj.subscribe === 'function'; +} diff --git a/packages/angular_devkit/core/src/utils/literals.ts b/packages/angular_devkit/core/src/utils/literals.ts index 13683c18a3..eff190829a 100644 --- a/packages/angular_devkit/core/src/utils/literals.ts +++ b/packages/angular_devkit/core/src/utils/literals.ts @@ -13,7 +13,7 @@ export type TemplateTag = (template: TemplateStringsArray, ...substitutions: any export function oneLine(strings: TemplateStringsArray, ...values: any[]) { const endResult = String.raw(strings, ...values); - return endResult.replace(/(?:\n(?:\s*))+/gm, ' ').trim(); + return endResult.replace(/(?:\r?\n(?:\s*))+/gm, ' ').trim(); } export function indentBy(indentations: number): TemplateTag { diff --git a/packages/angular_devkit/core/src/utils/object.ts b/packages/angular_devkit/core/src/utils/object.ts index b77c1a9596..b7282ddeb6 100644 --- a/packages/angular_devkit/core/src/utils/object.ts +++ b/packages/angular_devkit/core/src/utils/object.ts @@ -14,3 +14,24 @@ export function mapObject(obj: {[k: string]: T}, return acc; }, {}); } + +// tslint:disable-next-line:no-any +export function deepCopy (object: T): T { + if (Array.isArray(object)) { + // tslint:disable-next-line:no-any + return object.map((o: any) => deepCopy(o)); + } else if (typeof object === 'object') { + if (object['toJSON']) { + return JSON.parse((object['toJSON'] as () => string)()); + } + + const copy = {} as T; + for (const key of Object.keys(object)) { + copy[key] = deepCopy(object[key]); + } + + return copy; + } else { + return object; + } +} diff --git a/packages/angular_devkit/core/src/virtual-fs/host/alias.ts b/packages/angular_devkit/core/src/virtual-fs/host/alias.ts new file mode 100644 index 0000000000..c90e293a37 --- /dev/null +++ b/packages/angular_devkit/core/src/virtual-fs/host/alias.ts @@ -0,0 +1,132 @@ +/** + * @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 { Observable } from 'rxjs'; +import { NormalizedRoot, Path, PathFragment, join, split } from '../path'; +import { + FileBuffer, + Host, + HostCapabilities, + HostWatchEvent, + HostWatchOptions, + Stats, +} from './interface'; + + +/** + * A Virtual Host that allow to alias some paths to other paths. + * + * This does not verify, when setting an alias, that the target or source exist. Neither does it + * check whether it's a file or a directory. Please not that directories are also renamed/replaced. + * + * No recursion is done on the resolution, which means the following is perfectly valid then: + * + * ``` + * host.aliases.set(normalize('/file/a'), normalize('/file/b')); + * host.aliases.set(normalize('/file/b'), normalize('/file/a')); + * ``` + * + * This will result in a proper swap of two files for each others. + * + * @example + * const host = new SimpleMemoryHost(); + * host.write(normalize('/some/file'), content).subscribe(); + * + * const aHost = new AliasHost(host); + * aHost.read(normalize('/some/file')) + * .subscribe(x => expect(x).toBe(content)); + * aHost.aliases.set(normalize('/some/file'), normalize('/other/path'); + * + * // This file will not exist because /other/path does not exist. + * aHost.read(normalize('/some/file')) + * .subscribe(undefined, err => expect(err.message).toMatch(/does not exist/)); + * + * @example + * const host = new SimpleMemoryHost(); + * host.write(normalize('/some/folder/file'), content).subscribe(); + * + * const aHost = new AliasHost(host); + * aHost.read(normalize('/some/folder/file')) + * .subscribe(x => expect(x).toBe(content)); + * aHost.aliases.set(normalize('/some'), normalize('/other'); + * + * // This file will not exist because /other/path does not exist. + * aHost.read(normalize('/some/folder/file')) + * .subscribe(undefined, err => expect(err.message).toMatch(/does not exist/)); + * + * // Create the file with new content and verify that this has the new content. + * aHost.write(normalize('/other/folder/file'), content2).subscribe(); + * aHost.read(normalize('/some/folder/file')) + * .subscribe(x => expect(x).toBe(content2)); + */ +export class AliasHost implements Host { + protected _aliases = new Map(); + + constructor(protected _delegate: Host) {} + + protected _resolve(path: Path) { + let maybeAlias = this._aliases.get(path); + const sp = split(path); + const remaining: PathFragment[] = []; + + // Also resolve all parents of the requested files, only picking the first one that matches. + // This can have surprising behaviour when aliases are inside another alias. It will always + // use the closest one to the file. + while (!maybeAlias && sp.length > 0) { + const p = join(NormalizedRoot, ...sp); + maybeAlias = this._aliases.get(p); + + if (maybeAlias) { + maybeAlias = join(maybeAlias, ...remaining); + } + // Allow non-null-operator because we know sp.length > 0 (condition on while). + remaining.unshift(sp.pop() !); // tslint:disable-line:non-null-operator + } + + return maybeAlias || path; + } + + get aliases(): Map { return this._aliases; } + get capabilities(): HostCapabilities { return this._delegate.capabilities; } + + write(path: Path, content: FileBuffer): Observable { + return this._delegate.write(this._resolve(path), content); + } + read(path: Path): Observable { + return this._delegate.read(this._resolve(path)); + } + delete(path: Path): Observable { + return this._delegate.delete(this._resolve(path)); + } + rename(from: Path, to: Path): Observable { + return this._delegate.rename(this._resolve(from), this._resolve(to)); + } + + list(path: Path): Observable { + return this._delegate.list(this._resolve(path)); + } + + exists(path: Path): Observable { + return this._delegate.exists(this._resolve(path)); + } + isDirectory(path: Path): Observable { + return this._delegate.isDirectory(this._resolve(path)); + } + isFile(path: Path): Observable { + return this._delegate.isFile(this._resolve(path)); + } + + // Some hosts may not support stat. + stat(path: Path): Observable> | null { + return this._delegate.stat(this._resolve(path)); + } + + // Some hosts may not support watching. + watch(path: Path, options?: HostWatchOptions): Observable | null { + return this._delegate.watch(this._resolve(path), options); + } +} diff --git a/packages/angular_devkit/core/src/virtual-fs/host/alias_spec.ts b/packages/angular_devkit/core/src/virtual-fs/host/alias_spec.ts new file mode 100644 index 0000000000..6283b63e68 --- /dev/null +++ b/packages/angular_devkit/core/src/virtual-fs/host/alias_spec.ts @@ -0,0 +1,59 @@ +/** + * @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 { normalize } from '..'; +import { AliasHost } from './alias'; +import { stringToFileBuffer } from './buffer'; +import { SimpleMemoryHost } from './memory'; + +describe('AliasHost', () => { + it('works as in the example', () => { + const content = stringToFileBuffer('hello world'); + + const host = new SimpleMemoryHost(); + host.write(normalize('/some/file'), content).subscribe(); + + const aHost = new AliasHost(host); + aHost.read(normalize('/some/file')) + .subscribe(x => expect(x).toBe(content)); + aHost.aliases.set(normalize('/some/file'), normalize('/other/path')); + + // This file will not exist because /other/path does not exist. + try { + aHost.read(normalize('/some/file')) + .subscribe(undefined, err => { + expect(err.message).toMatch(/does not exist/); + }); + } catch (e) { + // Ignore it. RxJS <6 still throw errors when they happen synchronously. + } + }); + + it('works as in the example (2)', () => { + const content = stringToFileBuffer('hello world'); + const content2 = stringToFileBuffer('hello world 2'); + + const host = new SimpleMemoryHost(); + host.write(normalize('/some/folder/file'), content).subscribe(); + + const aHost = new AliasHost(host); + aHost.read(normalize('/some/folder/file')) + .subscribe(x => expect(x).toBe(content)); + aHost.aliases.set(normalize('/some'), normalize('/other')); + + // This file will not exist because /other/path does not exist. + try { + aHost.read(normalize('/some/folder/file')) + .subscribe(undefined, err => expect(err.message).toMatch(/does not exist/)); + } catch (e) {} + + // Create the file with new content and verify that this has the new content. + aHost.write(normalize('/other/folder/file'), content2).subscribe(); + aHost.read(normalize('/some/folder/file')) + .subscribe(x => expect(x).toBe(content2)); + }); +}); diff --git a/packages/angular_devkit/core/src/virtual-fs/host/buffer.ts b/packages/angular_devkit/core/src/virtual-fs/host/buffer.ts index c7b957571e..dbf9d39778 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/buffer.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/buffer.ts @@ -46,10 +46,29 @@ export function stringToFileBuffer(str: string): FileBuffer { } export function fileBufferToString(fileBuffer: FileBuffer): string { - if (typeof TextDecoder !== 'undefined') { + if (fileBuffer.toString.length == 1) { + return (fileBuffer.toString as (enc: string) => string)('utf-8'); + } else if (typeof Buffer !== 'undefined') { + return new Buffer(fileBuffer).toString('utf-8'); + } else if (typeof TextDecoder !== 'undefined') { // Modern browsers implement TextEncode. return new TextDecoder('utf-8').decode(new Uint8Array(fileBuffer)); } else { - return String.fromCharCode.apply(null, new Uint8Array(fileBuffer)); + // Slowest method but sure to be compatible with every platform. + const bufView = new Uint8Array(fileBuffer); + const bufLength = bufView.length; + let result = ''; + let chunkLength = Math.pow(2, 16) - 1; + + // We have to chunk it because String.fromCharCode.apply will throw + // `Maximum call stack size exceeded` on big inputs. + for (let i = 0; i < bufLength; i += chunkLength) { + if (i + chunkLength > bufLength) { + chunkLength = bufLength - i; + } + result += String.fromCharCode.apply(null, bufView.subarray(i, i + chunkLength)); + } + + return result; } } diff --git a/packages/angular_devkit/core/src/virtual-fs/host/index.ts b/packages/angular_devkit/core/src/virtual-fs/host/index.ts index e959f3df56..a49abead81 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/index.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/index.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +export * from './alias'; export * from './buffer'; export * from './interface'; export * from './memory'; +export * from './pattern'; export * from './scoped'; export * from './sync'; diff --git a/packages/angular_devkit/core/src/virtual-fs/host/interface.ts b/packages/angular_devkit/core/src/virtual-fs/host/interface.ts index 9d9d5372b9..57cb98a1f4 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/interface.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/interface.ts @@ -5,11 +5,12 @@ * Use 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 { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { Path, PathFragment } from '../path'; -export declare type FileBuffer = ArrayBuffer; +export type FileBuffer = ArrayBuffer; +export type FileBufferLike = ArrayBufferLike; export interface HostWatchOptions { readonly persistent?: boolean; @@ -24,7 +25,7 @@ export const enum HostWatchEventType { Renamed = 3, // Applied to the original file path. } -export type Stats = T & { +export type Stats = T & { isFile(): boolean; isDirectory(): boolean; @@ -49,7 +50,7 @@ export interface HostCapabilities { export interface Host { readonly capabilities: HostCapabilities; - write(path: Path, content: FileBuffer): Observable; + write(path: Path, content: FileBufferLike): Observable; read(path: Path): Observable; delete(path: Path): Observable; rename(from: Path, to: Path): Observable; @@ -60,8 +61,8 @@ export interface Host { isDirectory(path: Path): Observable; isFile(path: Path): Observable; - // Some hosts may not support stats. - stats(path: Path): Observable> | null; + // Some hosts may not support stat. + stat(path: Path): Observable> | null; // Some hosts may not support watching. watch(path: Path, options?: HostWatchOptions): Observable | null; diff --git a/packages/angular_devkit/core/src/virtual-fs/host/memory.ts b/packages/angular_devkit/core/src/virtual-fs/host/memory.ts index b39fd4d045..83e0bba3c1 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/memory.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/memory.ts @@ -5,11 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { empty } from 'rxjs/observable/empty'; -import { of as observableOf } from 'rxjs/observable/of'; -import { _throw } from 'rxjs/observable/throw'; +import { Observable, Subject } from 'rxjs'; import { FileAlreadyExistException, FileDoesNotExistException, @@ -22,7 +18,9 @@ import { Path, PathFragment, dirname, + isAbsolute, join, + normalize, split, } from '../path'; import { @@ -35,23 +33,49 @@ import { Stats, } from './interface'; +export interface SimpleMemoryHostStats { + readonly content: FileBuffer | null; +} export class SimpleMemoryHost implements Host<{}> { - private _cache = new Map(); + private _cache = new Map>(); private _watchers = new Map][]>(); - protected _isDir(path: Path) { - if (path === '/') { - return true; - } + protected _newDirStats() { + return { + isFile() { return false; }, + isDirectory() { return true; }, + size: 0, - for (const p of this._cache.keys()) { - if (p.startsWith(path + NormalizedSep)) { - return true; - } - } + atime: new Date(), + ctime: new Date(), + mtime: new Date(), + birthtime: new Date(), + + content: null, + }; + } + protected _newFileStats(content: FileBuffer, oldStats?: Stats) { + return { + isFile() { return true; }, + isDirectory() { return false; }, + size: content.byteLength, + + atime: oldStats ? oldStats.atime : new Date(), + ctime: new Date(), + mtime: new Date(), + birthtime: oldStats ? oldStats.birthtime : new Date(), - return false; + content, + }; + } + + constructor() { + this._cache.set(normalize('/'), this._newDirStats()); + } + + protected _toAbsolute(path: Path) { + return isAbsolute(path) ? path : normalize('/' + path); } protected _updateWatchers(path: Path, type: HostWatchEventType) { @@ -103,30 +127,53 @@ export class SimpleMemoryHost implements Host<{}> { return { synchronous: true }; } - write(path: Path, content: FileBuffer): Observable { - if (this._isDir(path)) { - return _throw(new PathIsDirectoryException(path)); + /** + * List of protected methods that give direct access outside the observables to the cache + * and internal states. + */ + protected _write(path: Path, content: FileBuffer): void { + path = this._toAbsolute(path); + const old = this._cache.get(path); + if (old && old.isDirectory()) { + throw new PathIsDirectoryException(path); } - const existed = this._cache.has(path); - this._cache.set(path, content); - this._updateWatchers(path, existed ? HostWatchEventType.Changed : HostWatchEventType.Created); + // Update all directories. If we find a file we know it's an invalid write. + const fragments = split(path); + let curr: Path = normalize('/'); + for (const fr of fragments) { + curr = join(curr, fr); + const maybeStats = this._cache.get(fr); + if (maybeStats) { + if (maybeStats.isFile()) { + throw new PathIsFileException(curr); + } + } else { + this._cache.set(curr, this._newDirStats()); + } + } - return empty(); + // Create the stats. + const stats: Stats = this._newFileStats(content, old); + this._cache.set(path, stats); + this._updateWatchers(path, old ? HostWatchEventType.Changed : HostWatchEventType.Created); } - read(path: Path): Observable { - if (this._isDir(path)) { - return _throw(new PathIsDirectoryException(path)); - } - const maybeBuffer = this._cache.get(path); - if (!maybeBuffer) { - return _throw(new FileDoesNotExistException(path)); + _read(path: Path): FileBuffer { + path = this._toAbsolute(path); + const maybeStats = this._cache.get(path); + if (!maybeStats) { + throw new FileDoesNotExistException(path); + } else if (maybeStats.isDirectory()) { + throw new PathIsDirectoryException(path); + } else if (!maybeStats.content) { + throw new PathIsDirectoryException(path); } else { - return observableOf(maybeBuffer); + return maybeStats.content; } } - delete(path: Path): Observable { - if (this._isDir(path)) { + _delete(path: Path): void { + path = this._toAbsolute(path); + if (this._isDirectory(path)) { for (const [cachePath, _] of this._cache.entries()) { if (path.startsWith(cachePath + NormalizedSep)) { this._cache.delete(cachePath); @@ -136,20 +183,22 @@ export class SimpleMemoryHost implements Host<{}> { this._cache.delete(path); } this._updateWatchers(path, HostWatchEventType.Deleted); - - return empty(); } - rename(from: Path, to: Path): Observable { + _rename(from: Path, to: Path): void { + from = this._toAbsolute(from); + to = this._toAbsolute(to); if (!this._cache.has(from)) { - return _throw(new FileDoesNotExistException(from)); + throw new FileDoesNotExistException(from); } else if (this._cache.has(to)) { - return _throw(new FileAlreadyExistException(from)); + throw new FileAlreadyExistException(to); } - if (this._isDir(from)) { + + if (this._isDirectory(from)) { for (const path of this._cache.keys()) { if (path.startsWith(from + NormalizedSep)) { const content = this._cache.get(path); if (content) { + // We don't need to clone or extract the content, since we're moving files. this._cache.set(join(to, NormalizedSep, path.slice(from.length)), content); } } @@ -163,14 +212,14 @@ export class SimpleMemoryHost implements Host<{}> { } this._updateWatchers(from, HostWatchEventType.Renamed); - - return empty(); } - list(path: Path): Observable { - if (this._cache.has(path)) { - return _throw(new PathIsFileException(path)); + _list(path: Path): PathFragment[] { + path = this._toAbsolute(path); + if (this._isFile(path)) { + throw new PathIsFileException(path); } + const fragments = split(path); const result = new Set(); if (path !== NormalizedRoot) { @@ -181,30 +230,110 @@ export class SimpleMemoryHost implements Host<{}> { } } else { for (const p of this._cache.keys()) { - if (p.startsWith(NormalizedSep)) { + if (p.startsWith(NormalizedSep) && p !== NormalizedRoot) { result.add(split(p)[1]); } } } - return observableOf([...result]); + return [...result]; + } + + _exists(path: Path): boolean { + return !!this._cache.get(this._toAbsolute(path)); + } + _isDirectory(path: Path): boolean { + const maybeStats = this._cache.get(this._toAbsolute(path)); + + return maybeStats ? maybeStats.isDirectory() : false; + } + _isFile(path: Path): boolean { + const maybeStats = this._cache.get(this._toAbsolute(path)); + + return maybeStats ? maybeStats.isFile() : false; + } + + _stat(path: Path): Stats { + const maybeStats = this._cache.get(this._toAbsolute(path)); + + if (!maybeStats) { + throw new FileDoesNotExistException(path); + } else { + return maybeStats; + } + } + + write(path: Path, content: FileBuffer): Observable { + return new Observable(obs => { + this._write(path, content); + obs.next(); + obs.complete(); + }); + } + + read(path: Path): Observable { + return new Observable(obs => { + const content = this._read(path); + obs.next(content); + obs.complete(); + }); + } + + delete(path: Path): Observable { + return new Observable(obs => { + this._delete(path); + obs.next(); + obs.complete(); + }); + } + + rename(from: Path, to: Path): Observable { + return new Observable(obs => { + this._rename(from, to); + obs.next(); + obs.complete(); + }); + } + + list(path: Path): Observable { + return new Observable(obs => { + obs.next(this._list(path)); + obs.complete(); + }); } exists(path: Path): Observable { - return observableOf(this._cache.has(path) || this._isDir(path)); + return new Observable(obs => { + obs.next(this._exists(path)); + obs.complete(); + }); } + isDirectory(path: Path): Observable { - return observableOf(this._isDir(path)); + return new Observable(obs => { + obs.next(this._isDirectory(path)); + obs.complete(); + }); } + isFile(path: Path): Observable { - return observableOf(this._cache.has(path)); + return new Observable(obs => { + obs.next(this._isFile(path)); + obs.complete(); + }); } - stats(_path: Path): Observable> | null { - return null; + // Some hosts may not support stat. + stat(path: Path): Observable> { + return new Observable>(obs => { + obs.next(this._stat(path)); + obs.complete(); + }); } watch(path: Path, options?: HostWatchOptions): Observable | null { + path = this._toAbsolute(path); + const subject = new Subject(); let maybeWatcherArray = this._watchers.get(path); if (!maybeWatcherArray) { diff --git a/packages/angular_devkit/core/src/virtual-fs/host/memory_spec.ts b/packages/angular_devkit/core/src/virtual-fs/host/memory_spec.ts index be1c2c936b..849829666d 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/memory_spec.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/memory_spec.ts @@ -93,6 +93,7 @@ describe('SimpleMemoryHost', () => { .toEqual([fragment('file1'), fragment('file2'), fragment('sub1')]); expect(host.list(normalize('/'))) .toEqual([fragment('sub'), fragment('file4')]); + expect(host.list(normalize('/inexistent'))).toEqual([]); }); it('supports isFile / isDirectory', () => { @@ -113,4 +114,21 @@ describe('SimpleMemoryHost', () => { expect(host.isDirectory(normalize('/sub/file1'))).toBe(false); expect(host.isDirectory(normalize('/sub/sub1/file3'))).toBe(false); }); + + it('makes every path absolute', () => { + const host = new SyncDelegateHost(new SimpleMemoryHost()); + + const buffer = stringToFileBuffer('hello'); + const buffer2 = stringToFileBuffer('hello 2'); + + host.write(normalize('file1'), buffer); + host.write(normalize('/sub/file2'), buffer); + host.write(normalize('sub/file2'), buffer2); + expect(host.isFile(normalize('file1'))).toBe(true); + expect(host.isFile(normalize('/file1'))).toBe(true); + expect(host.isFile(normalize('/sub/file2'))).toBe(true); + expect(host.read(normalize('sub/file2'))).toBe(buffer2); + expect(host.isDirectory(normalize('/sub'))).toBe(true); + expect(host.isDirectory(normalize('sub'))).toBe(true); + }); }); diff --git a/packages/angular_devkit/core/src/virtual-fs/host/pattern.ts b/packages/angular_devkit/core/src/virtual-fs/host/pattern.ts new file mode 100644 index 0000000000..8b6304f5d8 --- /dev/null +++ b/packages/angular_devkit/core/src/virtual-fs/host/pattern.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Observable } from 'rxjs'; +import { Path, PathFragment } from '../path'; +import { + FileBuffer, + Host, + HostCapabilities, + HostWatchEvent, + HostWatchOptions, + Stats, +} from './interface'; + + +export type ReplacementFunction = (path: Path) => Path; + + +/** + */ +export class PatternMatchingHost implements Host { + protected _patterns = new Map(); + + constructor(protected _delegate: Host) {} + + addPattern(pattern: string | string[], replacementFn: ReplacementFunction) { + // Simple GLOB pattern replacement. + const reString = '^(' + + (Array.isArray(pattern) ? pattern : [pattern]) + .map(ex => '(' + + ex.split(/[\/\\]/g).map(f => f + .replace(/[\-\[\]{}()+?.^$|]/g, '\\$&') + .replace(/^\*\*/g, '(.+?)?') + .replace(/\*/g, '[^/\\\\]*')) + .join('[\/\\\\]') + + ')') + .join('|') + + ')($|/|\\\\)'; + + this._patterns.set(new RegExp(reString), replacementFn); + } + + protected _resolve(path: Path) { + let newPath = path; + this._patterns.forEach((fn, re) => { + if (re.test(path)) { + newPath = fn(newPath); + } + }); + + return newPath; + } + + get capabilities(): HostCapabilities { return this._delegate.capabilities; } + + write(path: Path, content: FileBuffer): Observable { + return this._delegate.write(this._resolve(path), content); + } + read(path: Path): Observable { + return this._delegate.read(this._resolve(path)); + } + delete(path: Path): Observable { + return this._delegate.delete(this._resolve(path)); + } + rename(from: Path, to: Path): Observable { + return this._delegate.rename(this._resolve(from), this._resolve(to)); + } + + list(path: Path): Observable { + return this._delegate.list(this._resolve(path)); + } + + exists(path: Path): Observable { + return this._delegate.exists(this._resolve(path)); + } + isDirectory(path: Path): Observable { + return this._delegate.isDirectory(this._resolve(path)); + } + isFile(path: Path): Observable { + return this._delegate.isFile(this._resolve(path)); + } + + // Some hosts may not support stat. + stat(path: Path): Observable> | null { + return this._delegate.stat(this._resolve(path)); + } + + // Some hosts may not support watching. + watch(path: Path, options?: HostWatchOptions): Observable | null { + return this._delegate.watch(this._resolve(path), options); + } +} diff --git a/packages/angular_devkit/core/src/virtual-fs/host/pattern_spec.ts b/packages/angular_devkit/core/src/virtual-fs/host/pattern_spec.ts new file mode 100644 index 0000000000..23d2fcfd06 --- /dev/null +++ b/packages/angular_devkit/core/src/virtual-fs/host/pattern_spec.ts @@ -0,0 +1,44 @@ +/** + * @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 { normalize } from '..'; +import { stringToFileBuffer } from './buffer'; +import { SimpleMemoryHost } from './memory'; +import { PatternMatchingHost } from './pattern'; + +describe('PatternMatchingHost', () => { + it('works for NativeScript', () => { + const content = stringToFileBuffer('hello world'); + const content2 = stringToFileBuffer('hello world 2'); + + const host = new SimpleMemoryHost(); + host.write(normalize('/some/file.tns.ts'), content).subscribe(); + + const pHost = new PatternMatchingHost(host); + pHost.read(normalize('/some/file.tns.ts')) + .subscribe(x => expect(x).toBe(content)); + + pHost.addPattern('**/*.tns.ts', path => { + return normalize(path.replace(/\.tns\.ts$/, '.ts')); + }); + + // This file will not exist because /some/file.ts does not exist. + try { + pHost.read(normalize('/some/file.tns.ts')) + .subscribe(undefined, err => { + expect(err.message).toMatch(/does not exist/); + }); + } catch (e) { + // Ignore it. RxJS <6 still throw errors when they happen synchronously. + } + + // Create the file, it should exist now. + pHost.write(normalize('/some/file.ts'), content2).subscribe(); + pHost.read(normalize('/some/file.tns.ts')) + .subscribe(x => expect(x).toBe(content2)); + }); +}); diff --git a/packages/angular_devkit/core/src/virtual-fs/host/scoped.ts b/packages/angular_devkit/core/src/virtual-fs/host/scoped.ts index 94c84503c6..d50be7636c 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/scoped.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/scoped.ts @@ -5,7 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { NormalizedRoot, Path, PathFragment, join } from '../path'; import { FileBuffer, @@ -48,9 +48,9 @@ export class ScopedHost implements Host { return this._delegate.isFile(join(this._root, path)); } - // Some hosts may not support stats. - stats(path: Path): Observable> | null { - return this._delegate.stats(join(this._root, path)); + // Some hosts may not support stat. + stat(path: Path): Observable> | null { + return this._delegate.stat(join(this._root, path)); } // Some hosts may not support watching. diff --git a/packages/angular_devkit/core/src/virtual-fs/host/sync.ts b/packages/angular_devkit/core/src/virtual-fs/host/sync.ts index c5a126bf94..22037c4973 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/sync.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/sync.ts @@ -5,11 +5,12 @@ * Use 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 { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { BaseException } from '../../exception/exception'; import { Path, PathFragment } from '../path'; import { FileBuffer, + FileBufferLike, Host, HostCapabilities, HostWatchEvent, @@ -25,7 +26,7 @@ export class SynchronousDelegateExpectedException extends BaseException { /** * Implement a synchronous-only host interface (remove the Observable parts). */ -export class SyncDelegateHost { +export class SyncDelegateHost { constructor(protected _delegate: Host) { if (!_delegate.capabilities.synchronous) { throw new SynchronousDelegateExpectedException(); @@ -63,7 +64,7 @@ export class SyncDelegateHost { return this._delegate; } - write(path: Path, content: FileBuffer): void { + write(path: Path, content: FileBufferLike): void { return this._doSyncCall(this._delegate.write(path, content)); } read(path: Path): FileBuffer { @@ -90,9 +91,9 @@ export class SyncDelegateHost { return this._doSyncCall(this._delegate.isFile(path)); } - // Some hosts may not support stats. - stats(path: Path): Stats | null { - const result: Observable> | null = this._delegate.stats(path); + // Some hosts may not support stat. + stat(path: Path): Stats | null { + const result: Observable> | null = this._delegate.stat(path); if (result) { return this._doSyncCall(result); diff --git a/packages/angular_devkit/core/src/virtual-fs/host/test.ts b/packages/angular_devkit/core/src/virtual-fs/host/test.ts index 331a64fbe0..a24b4eb549 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/test.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/test.ts @@ -17,7 +17,7 @@ export class TestHost extends SimpleMemoryHost { super(); for (const filePath of Object.getOwnPropertyNames(map)) { - this.write(normalize(filePath), stringToFileBuffer(map[filePath])); + this._write(normalize(filePath), stringToFileBuffer(map[filePath])); } } diff --git a/packages/angular_devkit/core/src/virtual-fs/host/test_spec.ts b/packages/angular_devkit/core/src/virtual-fs/host/test_spec.ts index f7c10305cc..dc8556054a 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/test_spec.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/test_spec.ts @@ -5,8 +5,6 @@ * Use 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-implicit-dependencies -// tslint:disable:non-null-operator import { TestHost } from './test'; diff --git a/packages/angular_devkit/core/src/virtual-fs/path.ts b/packages/angular_devkit/core/src/virtual-fs/path.ts index e12b583f0d..9f80c9b9df 100644 --- a/packages/angular_devkit/core/src/virtual-fs/path.ts +++ b/packages/angular_devkit/core/src/virtual-fs/path.ts @@ -204,7 +204,7 @@ export function normalize(path: string): Path { // Match absolute windows path. const original = path; - if (path.match(/^[A-Z]:\\/)) { + if (path.match(/^[A-Z]:[\/\\]/)) { path = '\\' + path[0] + '\\' + path.substr(3); } diff --git a/packages/angular_devkit/core/src/workspace/index.ts b/packages/angular_devkit/core/src/workspace/index.ts new file mode 100644 index 0000000000..13517e107f --- /dev/null +++ b/packages/angular_devkit/core/src/workspace/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './workspace'; +export * from './workspace-schema'; diff --git a/packages/angular_devkit/architect/src/workspace-schema.json b/packages/angular_devkit/core/src/workspace/workspace-schema.json similarity index 51% rename from packages/angular_devkit/architect/src/workspace-schema.json rename to packages/angular_devkit/core/src/workspace/workspace-schema.json index 7cfaf9b80c..71acda0c48 100644 --- a/packages/angular_devkit/architect/src/workspace-schema.json +++ b/packages/angular_devkit/core/src/workspace/workspace-schema.json @@ -1,41 +1,45 @@ { "$schema": "http://json-schema.org/schema", - "id": "BuildFacaceWorkspaceSchema", - "title": "Workspace schema for validating a Architect workspace configuration file.", + "id": "AngularDevkitWorkspaceSchema", + "title": "Angular Devkit Workspace Schema for validating workspace JSON.", "type": "object", "properties": { "$schema": { "type": "string", "description": "Link to schema." }, - "name": { - "type": "string", - "description": "Workspace name." - }, "version": { "type": "number", "description": "Workspace Schema version." }, - "root": { + "newProjectRoot": { "type": "string", - "description": "Workspace root.", + "description": "New project root.", "default": "./" }, - "defaultProject": { - "type": "string", - "description": "Default target to run." + "cli": { + "$ref": "#/definitions/tool", + "default": {} + }, + "schematics": { + "$ref": "#/definitions/tool", + "default": {} + }, + "architect": { + "$ref": "#/definitions/tool", + "default": {} }, "projects": { "type": "object", "description": "A map of project names to project options.", "additionalProperties": { "$ref": "#/definitions/project" - } + }, + "default": {} } }, "additionalProperties": false, "required": [ - "name", "version", "projects" ], @@ -44,10 +48,6 @@ "type": "object", "description": "Project options.", "properties": { - "defaultTarget": { - "type": "string", - "description": "Default target to run." - }, "projectType": { "type": "string", "description": "Project type.", @@ -60,47 +60,39 @@ "type": "string", "description": "Root of the project sourcefiles." }, - "targets": { - "type": "object", - "description": "A map of available project targets.", - "additionalProperties": { - "$ref": "#/definitions/target" - } + "prefix": { + "type": "string", + "description": "The prefix to apply to generated selectors." + }, + "cli": { + "$ref": "#/definitions/tool", + "default": {} + }, + "schematics": { + "$ref": "#/definitions/tool", + "default": {} + }, + "architect": { + "$ref": "#/definitions/tool", + "default": {} } }, "additionalProperties": false, "required": [ - "projectType", - "root" + "root", + "projectType" ] }, - "target": { + "tool": { "type": "object", - "description": "Target options.", + "description": "Tool options.", "properties": { - "builder": { + "$schema": { "type": "string", - "description": "The builder used for this package." - }, - "options": { - "$ref": "#/definitions/options" - }, - "configurations": { - "type": "object", - "description": "A map of alternative target options.", - "additionalProperties": { - "$ref": "#/definitions/options" - } + "description": "Link to schema." } }, - "required": [ - "builder", - "options" - ] - }, - "options": { - "type": "object", - "description": "Target options." + "additionalProperties": true } } } diff --git a/packages/angular_devkit/core/src/workspace/workspace-schema.ts b/packages/angular_devkit/core/src/workspace/workspace-schema.ts new file mode 100644 index 0000000000..ba907905cc --- /dev/null +++ b/packages/angular_devkit/core/src/workspace/workspace-schema.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable +export interface WorkspaceSchema { + /** + * Link to schema. + */ + $schema?: string; + /** + * Workspace Schema version. + */ + version: number; + /** + * New project root. + */ + newProjectRoot?: string; + /** + * Tool options. + */ + cli?: { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; + }; + /** + * Tool options. + */ + schematics?: { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; + }; + /** + * Tool options. + */ + architect?: { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; + }; + /** + * A map of project names to project options. + */ + projects: { + [k: string]: Project; + }; +} +/** + * Project options. + */ +export interface Project { + /** + * Project type. + */ + projectType: "application" | "library"; + /** + * Root of the project sourcefiles. + */ + root: string; + /** + * The prefix to apply to generated selectors." + */ + prefix: string; + /** + * Tool options. + */ + cli?: { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; + }; + /** + * Tool options. + */ + schematics?: { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; + }; + /** + * Tool options. + */ + architect?: Architect; +} +/** + * Architect options. + */ +export interface Architect { + /** + * Link to schema. + */ + $schema?: string; + [k: string]: any; +} diff --git a/packages/angular_devkit/core/src/workspace/workspace.ts b/packages/angular_devkit/core/src/workspace/workspace.ts new file mode 100644 index 0000000000..f863cd78ec --- /dev/null +++ b/packages/angular_devkit/core/src/workspace/workspace.ts @@ -0,0 +1,231 @@ +/** + * @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 { Observable, of, throwError } from 'rxjs'; +import { concatMap, map, tap } from 'rxjs/operators'; +import { + JsonObject, + JsonParseMode, + Path, + join, + normalize, + parseJson, + schema, + virtualFs, +} from '..'; +import { BaseException } from '../exception/exception'; +// Note: importing BaseException from '..' seems to lead to odd circular dependency errors. +// TypeError: Class extends value undefined is not a constructor or null +// at Object. (\packages\angular_devkit\core\src\workspace\workspace.ts:19:44) + + +export class ProjectNotFoundException extends BaseException { + constructor(name: string) { + super(`Project '${name}' could not be found in workspace.`); + } +} + +export class WorkspaceToolNotFoundException extends BaseException { + constructor(name: string) { + super(`Tool ${name} could not be found in workspace.`); + } +} + +export class ProjectToolNotFoundException extends BaseException { + constructor(name: string) { + super(`Tool ${name} could not be found in project.`); + } +} + +export class WorkspaceNotYetLoadedException extends BaseException { + constructor() { super(`Workspace needs to be loaded before it is used.`); } +} + +export interface WorkspaceJson { + version: number; + // TODO: figure out if newProjectRoot should stay here. + newProjectRoot: Path; + cli: WorkspaceTool; + schematics: WorkspaceTool; + architect: WorkspaceTool; + projects: { [k: string]: WorkspaceProject }; +} + +export interface WorkspaceProject { + projectType: 'application' | 'library'; + root: Path; + cli: WorkspaceTool; + schematics: WorkspaceTool; + architect: WorkspaceTool; +} + +export interface WorkspaceTool extends JsonObject { } + +export class Workspace { + private readonly _workspaceSchemaPath = join(normalize(__dirname), 'workspace-schema.json'); + private _workspaceSchema: JsonObject; + private _workspace: WorkspaceJson; + private _registry: schema.CoreSchemaRegistry; + + constructor(private _root: Path, private _host: virtualFs.Host<{}>) { + this._registry = new schema.CoreSchemaRegistry(); + } + + loadWorkspaceFromJson(json: {}) { + return this._loadWorkspaceSchema().pipe( + concatMap((workspaceSchema) => this.validateAgainstSchema(json, workspaceSchema)), + tap((validatedWorkspace: WorkspaceJson) => this._workspace = validatedWorkspace), + map(() => this), + ); + } + + loadWorkspaceFromHost(workspacePath: Path) { + return this._loadWorkspaceSchema().pipe( + concatMap(() => this._loadJsonFile(join(this._root, workspacePath))), + concatMap(json => this.loadWorkspaceFromJson(json)), + ); + } + + private _loadWorkspaceSchema() { + if (this._workspaceSchema) { + return of(this._workspaceSchema); + } else { + return this._loadJsonFile(this._workspaceSchemaPath).pipe( + tap((workspaceSchema) => this._workspaceSchema = workspaceSchema), + ); + } + } + + private _assertLoaded() { + if (!this._workspace) { + throw new WorkspaceNotYetLoadedException(); + } + } + + get root() { + return this._root; + } + + get host() { + return this._host; + } + + get version() { + this._assertLoaded(); + + return this._workspace.version; + } + + get newProjectRoot() { + this._assertLoaded(); + + return this._workspace.newProjectRoot; + } + + listProjectNames(): string[] { + return Object.keys(this._workspace.projects); + } + + getProject(projectName: string): WorkspaceProject { + this._assertLoaded(); + + const workspaceProject = this._workspace.projects[projectName]; + + if (!workspaceProject) { + throw new ProjectNotFoundException(projectName); + } + + return { + ...workspaceProject, + // Return only the project properties, and remove the tools. + cli: {}, + schematics: {}, + architect: {}, + }; + } + + getCli() { + return this._getTool('cli'); + } + + getSchematics() { + return this._getTool('schematics'); + } + + getArchitect() { + return this._getTool('architect'); + } + + getProjectCli(projectName: string) { + return this._getProjectTool(projectName, 'cli'); + } + + getProjectSchematics(projectName: string) { + return this._getProjectTool(projectName, 'schematics'); + } + + getProjectArchitect(projectName: string) { + return this._getProjectTool(projectName, 'architect'); + } + + private _getTool(toolName: 'cli' | 'schematics' | 'architect'): WorkspaceTool { + this._assertLoaded(); + + const workspaceTool = this._workspace[toolName]; + + if (!workspaceTool) { + throw new WorkspaceToolNotFoundException(toolName); + } + + return workspaceTool; + } + + private _getProjectTool( + projectName: string, toolName: 'cli' | 'schematics' | 'architect', + ): WorkspaceTool { + this._assertLoaded(); + + const workspaceProject = this._workspace.projects[projectName]; + + if (!workspaceProject) { + throw new ProjectNotFoundException(projectName); + } + + const projectTool = workspaceProject[toolName]; + + if (!projectTool) { + throw new ProjectToolNotFoundException(toolName); + } + + return projectTool; + } + + // TODO: add transforms to resolve paths. + validateAgainstSchema(contentJson: {}, schemaJson: JsonObject): Observable { + // JSON validation modifies the content, so we validate a copy of it instead. + const contentJsonCopy = JSON.parse(JSON.stringify(contentJson)); + + return this._registry.compile(schemaJson).pipe( + concatMap(validator => validator(contentJsonCopy)), + concatMap(validatorResult => { + if (validatorResult.success) { + return of(contentJsonCopy as T); + } else { + return throwError(new schema.SchemaValidationException(validatorResult.errors)); + } + }), + ); + } + + private _loadJsonFile(path: Path): Observable { + return this._host.read(normalize(path)).pipe( + map(buffer => virtualFs.fileBufferToString(buffer)), + map(str => parseJson(str, JsonParseMode.Loose) as {} as JsonObject), + ); + } +} diff --git a/packages/angular_devkit/core/src/workspace/workspace_spec.ts b/packages/angular_devkit/core/src/workspace/workspace_spec.ts new file mode 100644 index 0000000000..e22b3e4860 --- /dev/null +++ b/packages/angular_devkit/core/src/workspace/workspace_spec.ts @@ -0,0 +1,232 @@ +/** + * @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 { tap } from 'rxjs/operators'; +import { schema } from '..'; +import { NodeJsSyncHost } from '../../node'; +import { join, normalize } from '../virtual-fs'; +import { + ProjectNotFoundException, + Workspace, + WorkspaceNotYetLoadedException, + WorkspaceProject, +} from './workspace'; + + +describe('Workspace', () => { + const host = new NodeJsSyncHost(); + const root = normalize(__dirname); + // The content of this JSON object should be kept in sync with the path below: + // tests/@angular_devkit/workspace/angular-workspace.json + const workspaceJson = { + version: 1, + newProjectRoot: './projects', + cli: { + '$globalOverride': '${HOME}/.angular-cli.json', + 'schematics': { + 'defaultCollection': '@schematics/angular', + }, + 'warnings': { + 'showDeprecation': false, + }, + }, + schematics: { + '@schematics/angular': { + '*': { + skipImport: true, + 'packageManager': 'yarn', + }, + 'application': { + spec: false, + }, + }, + }, + architect: {}, + projects: { + app: { + root: 'projects/app', + projectType: 'application', + prefix: 'app', + cli: {}, + schematics: { + '@schematics/angular': { + '*': { + spec: false, + }, + }, + }, + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + transforms: [ + { + plugin: '@angular-devkit/architect-transforms:replacement', + file: 'environments/environment.ts', + configurations: { + production: 'environments/environment.prod.ts', + }, + }, + ], + options: { + outputPath: '../dist', + index: 'index.html', + main: 'main.ts', + polyfills: 'polyfills.ts', + tsConfig: 'tsconfig.app.json', + progress: false, + }, + configurations: { + production: { + optimize: true, + outputHashing: 'all', + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + }, + }, + }, + }, + }, + }, + }; + + it('loads workspace from json', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getProject('app').root).toEqual(workspaceJson.projects['app'].root)), + ).subscribe(undefined, done.fail, done); + }); + + it('loads workspace from host', (done) => { + const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any + const workspaceRoot = join(devkitRoot, 'tests/@angular_devkit/core/workspace'); + const workspace = new Workspace(workspaceRoot, host); + workspace.loadWorkspaceFromHost(normalize('angular-workspace.json')).pipe( + tap((ws) => expect(ws.getProject('app').root).toEqual(workspaceJson.projects['app'].root)), + ).subscribe(undefined, done.fail, done); + }); + + it('errors when workspace fails validation', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson({ foo: 'bar' }) + .subscribe(undefined, (err) => { + expect(err).toEqual(jasmine.any(schema.SchemaValidationException)); + done(); + }, done.fail); + }); + + it('throws when getting information before workspace is loaded', () => { + const workspace = new Workspace(root, host); + expect(() => workspace.version).toThrow(new WorkspaceNotYetLoadedException()); + }); + + it('throws when getting workspace tool before workspace is loaded', () => { + const workspace = new Workspace(root, host); + expect(() => workspace.getCli()).toThrow(new WorkspaceNotYetLoadedException()); + }); + + it('gets workspace root', () => { + const workspace = new Workspace(root, host); + expect(workspace.root).toBe(root); + }); + + it('gets workspace host', () => { + const workspace = new Workspace(root, host); + expect(workspace.host).toBe(host); + }); + + it('gets workspace version', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.version).toEqual(workspaceJson.version)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets workspace new project root', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.newProjectRoot).toEqual(workspaceJson.newProjectRoot)), + ).subscribe(undefined, done.fail, done); + }); + + it('lists project names', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.listProjectNames()).toEqual(['app'])), + ).subscribe(undefined, done.fail, done); + }); + + it('gets project by name', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getProject('app')).toEqual({ + ...workspaceJson.projects['app'], + // Tools should not be returned when getting a project. + cli: {}, + schematics: {}, + architect: {}, + } as {} as WorkspaceProject)), + ).subscribe(undefined, done.fail, done); + }); + + it('throws on missing project', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(() => ws.getProject('abc')).toThrow(new ProjectNotFoundException('abc'))), + ).subscribe(undefined, done.fail, done); + }); + + it('gets workspace cli', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getCli()).toEqual(workspaceJson.cli)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets workspace schematics', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getSchematics()).toEqual(workspaceJson.schematics)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets workspace architect', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getArchitect()).toEqual(workspaceJson.architect)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets project cli', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getProjectCli('app')).toEqual(workspaceJson.projects.app.cli)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets project schematics', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getProjectSchematics('app')) + .toEqual(workspaceJson.projects.app.schematics)), + ).subscribe(undefined, done.fail, done); + }); + + it('gets project architect', (done) => { + const workspace = new Workspace(root, host); + workspace.loadWorkspaceFromJson(workspaceJson).pipe( + tap((ws) => expect(ws.getProjectArchitect('app')) + .toEqual(workspaceJson.projects.app.architect)), + ).subscribe(undefined, done.fail, done); + }); + +}); diff --git a/packages/angular_devkit/schematics/BUILD b/packages/angular_devkit/schematics/BUILD index 9330a9ef25..ca9130b579 100644 --- a/packages/angular_devkit/schematics/BUILD +++ b/packages/angular_devkit/schematics/BUILD @@ -59,6 +59,7 @@ ts_library( module_root = "tools", deps = [ ":schematics", + ":tasks", "//packages/angular_devkit/core", "//packages/angular_devkit/core:node", # @deps: rxjs diff --git a/packages/angular_devkit/schematics/package.json b/packages/angular_devkit/schematics/package.json index 041a541fd2..c3cfde8549 100644 --- a/packages/angular_devkit/schematics/package.json +++ b/packages/angular_devkit/schematics/package.json @@ -16,10 +16,8 @@ "schematics" ], "dependencies": { + "@angular-devkit/core": "0.0.0", "@ngtools/json-schema": "^1.1.0", - "rxjs": "^5.5.6" - }, - "peerDependencies": { - "@angular-devkit/core": "0.0.0" + "rxjs": "^6.0.0-beta.3" } } diff --git a/packages/angular_devkit/schematics/src/engine/engine.ts b/packages/angular_devkit/schematics/src/engine/engine.ts index 8981790dda..f168e47261 100644 --- a/packages/angular_devkit/schematics/src/engine/engine.ts +++ b/packages/angular_devkit/schematics/src/engine/engine.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ import { BaseException, logging } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { from as observableFrom } from 'rxjs/observable/from'; +import { Observable, from as observableFrom } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { Url } from 'url'; import { MergeStrategy } from '../tree/interface'; import { NullTree } from '../tree/null'; import { empty } from '../tree/static'; +import { Workflow } from '../workflow'; import { CollectionImpl } from './collection'; import { Collection, @@ -78,9 +78,10 @@ export class SchematicEngine>>(); private _taskSchedulers = new Array(); - constructor(private _host: EngineHost) { + constructor(private _host: EngineHost, protected _workflow?: Workflow) { } + get workflow() { return this._workflow || null; } get defaultMergeStrategy() { return this._host.defaultMergeStrategy || MergeStrategy.Default; } createCollection(name: string): Collection { @@ -132,7 +133,7 @@ export class SchematicEngine = { debug: parent && parent.debug || false, engine: this, logger: (parent && parent.logger && parent.logger.createChild(schematic.description.name)) @@ -143,6 +144,11 @@ export class SchematicEngine, options: OptionT, ): Observable; + transformContext( + context: TypedSchematicContext, + ): TypedSchematicContext | void; createTaskExecutor(name: string): Observable; hasTaskExecutor(name: string): boolean; @@ -102,6 +106,7 @@ export interface Engine; readonly defaultMergeStrategy: MergeStrategy; + readonly workflow: Workflow | null; } diff --git a/packages/angular_devkit/schematics/src/engine/schematic.ts b/packages/angular_devkit/schematics/src/engine/schematic.ts index ed385cbeea..932fdd7628 100644 --- a/packages/angular_devkit/schematics/src/engine/schematic.ts +++ b/packages/angular_devkit/schematics/src/engine/schematic.ts @@ -6,11 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import { BaseException } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; -import { concatMap } from 'rxjs/operators/concatMap'; -import { first } from 'rxjs/operators/first'; -import { map } from 'rxjs/operators/map'; +import { Observable, of as observableOf } from 'rxjs'; +import { concatMap, first, map } from 'rxjs/operators'; import { callRule } from '../rules/call'; import { Tree } from '../tree/interface'; import { diff --git a/packages/angular_devkit/schematics/src/engine/schematic_spec.ts b/packages/angular_devkit/schematics/src/engine/schematic_spec.ts index 5f4c2a341f..5507e8e90e 100644 --- a/packages/angular_devkit/schematics/src/engine/schematic_spec.ts +++ b/packages/angular_devkit/schematics/src/engine/schematic_spec.ts @@ -7,7 +7,7 @@ */ // tslint:disable:non-null-operator import { logging } from '@angular-devkit/core'; -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { MergeStrategy, Tree } from '../tree/interface'; import { branch, empty } from '../tree/static'; import { CollectionDescription, Engine, Rule, Schematic, SchematicDescription } from './interface'; diff --git a/packages/angular_devkit/schematics/src/engine/task.ts b/packages/angular_devkit/schematics/src/engine/task.ts index 51029bf6c2..bff301f8d6 100644 --- a/packages/angular_devkit/schematics/src/engine/task.ts +++ b/packages/angular_devkit/schematics/src/engine/task.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { BaseException, PriorityQueue } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { SchematicContext } from './interface'; export class UnknownTaskDependencyException extends BaseException { diff --git a/packages/angular_devkit/schematics/src/exception/exception.ts b/packages/angular_devkit/schematics/src/exception/exception.ts index 6be8c85d7b..f7f419bfc1 100644 --- a/packages/angular_devkit/schematics/src/exception/exception.ts +++ b/packages/angular_devkit/schematics/src/exception/exception.ts @@ -33,6 +33,12 @@ export class MergeConflictException extends BaseException { } } +export class UnsuccessfulWorkflowExecution extends BaseException { + constructor() { + super('Workflow did not execute successfully.'); + } +} + export class UnimplementedException extends BaseException { constructor() { super('This function is unimplemented.'); } } diff --git a/packages/angular_devkit/schematics/src/formats/format-validator.ts b/packages/angular_devkit/schematics/src/formats/format-validator.ts index 2bf673772d..863a5b3e21 100644 --- a/packages/angular_devkit/schematics/src/formats/format-validator.ts +++ b/packages/angular_devkit/schematics/src/formats/format-validator.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { JsonObject, JsonValue, schema } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; diff --git a/packages/angular_devkit/schematics/src/index.ts b/packages/angular_devkit/schematics/src/index.ts index d639a3aaa0..370f76bff3 100644 --- a/packages/angular_devkit/schematics/src/index.ts +++ b/packages/angular_devkit/schematics/src/index.ts @@ -26,6 +26,7 @@ export { export * from './exception/exception'; export * from './tree/interface'; export * from './rules/base'; +export * from './rules/call'; export * from './rules/move'; export * from './rules/random'; export * from './rules/schematic'; @@ -40,9 +41,13 @@ export * from './engine/schematic'; export * from './sink/dryrun'; export * from './sink/filesystem'; export * from './sink/host'; +export * from './sink/sink'; + import * as formats from './formats'; export { formats }; +import * as workflow from './workflow'; +export { workflow }; export interface TreeConstructor { empty(): TreeInterface; diff --git a/packages/angular_devkit/schematics/src/rules/base.ts b/packages/angular_devkit/schematics/src/rules/base.ts index 9598251ca5..347d2ba833 100644 --- a/packages/angular_devkit/schematics/src/rules/base.ts +++ b/packages/angular_devkit/schematics/src/rules/base.ts @@ -5,8 +5,7 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; +import { Observable, of as observableOf } from 'rxjs'; import { concatMap, map } from 'rxjs/operators'; import { FileOperator, Rule, SchematicContext, Source } from '../engine/interface'; import { FilteredTree } from '../tree/filtered'; diff --git a/packages/angular_devkit/schematics/src/rules/base_spec.ts b/packages/angular_devkit/schematics/src/rules/base_spec.ts index 8bf8d9becc..f425bfd327 100644 --- a/packages/angular_devkit/schematics/src/rules/base_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/base_spec.ts @@ -13,7 +13,7 @@ import { MergeStrategy, partitionApplyMerge, } from '@angular-devkit/schematics'; -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { Rule, SchematicContext, Source } from '../engine/interface'; import { Tree } from '../tree/interface'; import { empty } from '../tree/static'; diff --git a/packages/angular_devkit/schematics/src/rules/call.ts b/packages/angular_devkit/schematics/src/rules/call.ts index d155c34edc..6a94a9b32e 100644 --- a/packages/angular_devkit/schematics/src/rules/call.ts +++ b/packages/angular_devkit/schematics/src/rules/call.ts @@ -5,22 +5,13 @@ * Use 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 } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; -import { _throw } from 'rxjs/observable/throw'; -import { last } from 'rxjs/operators/last'; -import { mergeMap } from 'rxjs/operators/mergeMap'; -import { tap } from 'rxjs/operators/tap'; +import { BaseException, isObservable } from '@angular-devkit/core'; +import { Observable, of as observableOf, throwError } from 'rxjs'; +import { last, mergeMap, tap } from 'rxjs/operators'; import { Rule, SchematicContext, Source } from '../engine/interface'; import { Tree, TreeSymbol } from '../tree/interface'; -declare const Symbol: Symbol & { - readonly observable: symbol; -}; - - function _getTypeOfResult(value?: {}): string { if (value === undefined) { return 'undefined'; @@ -63,10 +54,10 @@ export function callSource(source: Source, context: SchematicContext): Observabl const result = source(context) as object; if (result === undefined) { - return _throw(new InvalidSourceResultException(result)); + return throwError(new InvalidSourceResultException(result)); } else if (TreeSymbol in result) { return observableOf(result as Tree); - } else if (Symbol.observable in result) { + } else if (isObservable(result)) { // Only return the last Tree, and make sure it's a Tree. return (result as Observable).pipe( last(), @@ -77,7 +68,7 @@ export function callSource(source: Source, context: SchematicContext): Observabl }), ); } else { - return _throw(new InvalidSourceResultException(result)); + return throwError(new InvalidSourceResultException(result)); } } @@ -92,7 +83,7 @@ export function callRule(rule: Rule, return observableOf(inputTree); } else if (TreeSymbol in result) { return observableOf(result as Tree); - } else if (Symbol.observable in result) { + } else if (isObservable(result)) { const obs = result as Observable; // Only return the last Tree, and make sure it's a Tree. @@ -107,7 +98,7 @@ export function callRule(rule: Rule, } else if (result === undefined) { return observableOf(inputTree); } else { - return _throw(new InvalidRuleResultException(result)); + return throwError(new InvalidRuleResultException(result)); } })); } diff --git a/packages/angular_devkit/schematics/src/rules/call_spec.ts b/packages/angular_devkit/schematics/src/rules/call_spec.ts index 743a394608..4d97a21c2d 100644 --- a/packages/angular_devkit/schematics/src/rules/call_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/call_spec.ts @@ -9,7 +9,7 @@ // tslint:disable:no-any // tslint:disable:no-implicit-dependencies import { MergeStrategy } from '@angular-devkit/schematics'; -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { Rule, SchematicContext, Source } from '../engine/interface'; import { Tree } from '../tree/interface'; import { empty } from '../tree/static'; diff --git a/packages/angular_devkit/schematics/src/rules/move_spec.ts b/packages/angular_devkit/schematics/src/rules/move_spec.ts index e54d7d448a..c6ceaab66c 100644 --- a/packages/angular_devkit/schematics/src/rules/move_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/move_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ // tslint:disable:non-null-operator -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { SchematicContext } from '../engine/interface'; import { VirtualTree } from '../tree/virtual'; import { callRule } from './call'; diff --git a/packages/angular_devkit/schematics/src/rules/schematic.ts b/packages/angular_devkit/schematics/src/rules/schematic.ts index 2aabae9ef2..32d7492f90 100644 --- a/packages/angular_devkit/schematics/src/rules/schematic.ts +++ b/packages/angular_devkit/schematics/src/rules/schematic.ts @@ -5,7 +5,7 @@ * Use 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 { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; import { Rule, SchematicContext } from '../engine/interface'; import { Tree } from '../tree/interface'; import { branch } from '../tree/static'; diff --git a/packages/angular_devkit/schematics/src/sink/dryrun.ts b/packages/angular_devkit/schematics/src/sink/dryrun.ts index 48511cd078..4c52b33701 100644 --- a/packages/angular_devkit/schematics/src/sink/dryrun.ts +++ b/packages/angular_devkit/schematics/src/sink/dryrun.ts @@ -7,9 +7,7 @@ */ import { normalize, virtualFs } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { empty } from 'rxjs/observable/empty'; +import { Observable, Subject, of } from 'rxjs'; import { HostSink } from './host'; @@ -105,6 +103,9 @@ export class DryRunSink extends HostSink { this._subject.next({ kind: 'delete', path }); }); + this._filesToRename.forEach(([path, to]) => { + this._subject.next({ kind: 'rename', path, to }); + }); this._filesToCreate.forEach((content, path) => { // Check if this is a renaming. for (const [_, to] of this._filesToRename) { @@ -123,12 +124,9 @@ export class DryRunSink extends HostSink { this._filesToUpdate.forEach((content, path) => { this._subject.next({ kind: 'update', path, content: content.generate() }); }); - this._filesToRename.forEach(([path, to]) => { - this._subject.next({ kind: 'rename', path, to }); - }); this._subject.complete(); - return empty(); + return of(undefined); } } diff --git a/packages/angular_devkit/schematics/src/sink/dryrun_spec.ts b/packages/angular_devkit/schematics/src/sink/dryrun_spec.ts index 129992c640..34db778167 100644 --- a/packages/angular_devkit/schematics/src/sink/dryrun_spec.ts +++ b/packages/angular_devkit/schematics/src/sink/dryrun_spec.ts @@ -62,7 +62,7 @@ describe('DryRunSink', () => { // Need to create this file on the filesystem, otherwise the commit phase will fail. const outputHost = new virtualFs.SimpleMemoryHost(); - outputHost.write(normalize('/hello'), virtualFs.stringToFileBuffer('')); + outputHost.write(normalize('/hello'), virtualFs.stringToFileBuffer('')).subscribe(); const sink = new DryRunSink(outputHost); sink.reporter.pipe(toArray()) diff --git a/packages/angular_devkit/schematics/src/sink/host.ts b/packages/angular_devkit/schematics/src/sink/host.ts index 4345194c1f..aa498ea2a6 100644 --- a/packages/angular_devkit/schematics/src/sink/host.ts +++ b/packages/angular_devkit/schematics/src/sink/host.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ import { Path, virtualFs } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { concat as concatObservables } from 'rxjs/observable/concat'; -import { empty } from 'rxjs/observable/empty'; -import { from as observableFrom } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; +import { + EMPTY, + Observable, + concat as concatObservables, + from as observableFrom, + of as observableOf, +} from 'rxjs'; import { concatMap, reduce } from 'rxjs/operators'; import { CreateFileAction } from '../tree/action'; import { UpdateBuffer } from '../utility/update-buffer'; @@ -26,7 +28,7 @@ export class HostSink extends SimpleSinkBase { constructor(protected _host: virtualFs.Host, protected _force = false) { super(); } protected _validateCreateAction(action: CreateFileAction): Observable { - return this._force ? empty() : super._validateCreateAction(action); + return this._force ? EMPTY : super._validateCreateAction(action); } protected _validateFileExists(p: Path): Observable { @@ -34,6 +36,8 @@ export class HostSink extends SimpleSinkBase { return observableOf(true); } else if (this._filesToDelete.has(p)) { return observableOf(false); + } else if ([...this._filesToRename.values()].some(([from]) => from == p)) { + return observableOf(false); } else { return this._host.exists(p); } @@ -42,17 +46,17 @@ export class HostSink extends SimpleSinkBase { protected _overwriteFile(path: Path, content: Buffer): Observable { this._filesToUpdate.set(path, new UpdateBuffer(content)); - return empty(); + return EMPTY; } protected _createFile(path: Path, content: Buffer): Observable { this._filesToCreate.set(path, new UpdateBuffer(content)); - return empty(); + return EMPTY; } protected _renameFile(from: Path, to: Path): Observable { this._filesToRename.add([from, to]); - return empty(); + return EMPTY; } protected _deleteFile(path: Path): Observable { if (this._filesToCreate.has(path)) { @@ -62,20 +66,22 @@ export class HostSink extends SimpleSinkBase { this._filesToDelete.add(path); } - return empty(); + return EMPTY; } _done() { // Really commit everything to the actual filesystem. return concatObservables( observableFrom([...this._filesToDelete.values()]).pipe( - concatMap(path => this._host.delete(path))), + concatMap(path => this._host.delete(path)), + ), + observableFrom([...this._filesToRename.entries()]).pipe( + concatMap(([_, [path, to]]) => this._host.rename(path, to)), + ), observableFrom([...this._filesToCreate.entries()]).pipe( concatMap(([path, buffer]) => { return this._host.write(path, buffer.generate() as {} as virtualFs.FileBuffer); })), - observableFrom([...this._filesToRename.entries()]).pipe( - concatMap(([_, [path, to]]) => this._host.rename(path, to))), observableFrom([...this._filesToUpdate.entries()]).pipe( concatMap(([path, buffer]) => { return this._host.write(path, buffer.generate() as {} as virtualFs.FileBuffer); diff --git a/packages/angular_devkit/schematics/src/sink/host_spec.ts b/packages/angular_devkit/schematics/src/sink/host_spec.ts index d850ec12a1..f8ee665968 100644 --- a/packages/angular_devkit/schematics/src/sink/host_spec.ts +++ b/packages/angular_devkit/schematics/src/sink/host_spec.ts @@ -8,6 +8,7 @@ // tslint:disable:no-implicit-dependencies import { normalize, virtualFs } from '@angular-devkit/core'; import { FileSystemTree, HostSink } from '@angular-devkit/schematics'; +import { fileBufferToString } from '../../../core/src/virtual-fs/host'; import { FileSystemCreateTree } from '../tree/filesystem'; import { optimize } from '../tree/static'; @@ -110,5 +111,23 @@ describe('FileSystemSink', () => { }) .then(done, done.fail); }); + + it('can rename then create the same file', done => { + const host = new virtualFs.test.TestHost({ + '/file0': 'world', + }); + const tree = new FileSystemTree(host); + tree.rename('/file0', '/file1'); + tree.create('/file0', 'hello'); + + const sink = new HostSink(host); + sink.commit(optimize(tree)) + .toPromise() + .then(() => { + expect(host.sync.read(normalize('/file0')).toString()).toBe('hello'); + expect(fileBufferToString(host.sync.read(normalize('/file1')))).toBe('world'); + }) + .then(done, done.fail); + }); }); }); diff --git a/packages/angular_devkit/schematics/src/sink/sink.ts b/packages/angular_devkit/schematics/src/sink/sink.ts index beb4bf1412..30f0c53175 100644 --- a/packages/angular_devkit/schematics/src/sink/sink.ts +++ b/packages/angular_devkit/schematics/src/sink/sink.ts @@ -5,13 +5,14 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { defer as deferObservable } from 'rxjs/observable/defer'; -import { empty } from 'rxjs/observable/empty'; -import { from as observableFrom } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; import { + Observable, concat, + defer as deferObservable, + from as observableFrom, + of as observableOf, +} from 'rxjs'; +import { concatMap, ignoreElements, map, @@ -97,11 +98,9 @@ export abstract class SimpleSinkBase implements Sink { } commitSingleAction(action: Action): Observable { - return empty().pipe( - concat(new Observable(observer => { - return this.validateSingleAction(action).subscribe(observer); - })), - concat(new Observable(observer => { + return concat( + this.validateSingleAction(action), + new Observable(observer => { let committed = null; switch (action.kind) { case 'o': committed = this._overwriteFile(action.path, action.content); break; @@ -115,31 +114,36 @@ export abstract class SimpleSinkBase implements Sink { } else { observer.complete(); } - }))); + }), + ).pipe(ignoreElements()); } commit(tree: Tree): Observable { const actions = observableFrom(tree.actions); - return (this.preCommit() || empty()).pipe( - concat(deferObservable(() => actions)), - concatMap((action: Action) => { - const maybeAction = this.preCommitAction(action); - if (!maybeAction) { - return observableOf(action); - } else if (isAction(maybeAction)) { - return observableOf(maybeAction); - } else { - return maybeAction; - } - }), - concatMap((action: Action) => { - return this.commitSingleAction(action).pipe( - ignoreElements(), - concat([action])); - }), - concatMap((action: Action) => this.postCommitAction(action) || empty()), - concat(deferObservable(() => this._done())), - concat(deferObservable(() => this.postCommit() || empty()))); + return concat( + (this.preCommit() || observableOf(null)), + deferObservable(() => actions).pipe( + concatMap(action => { + const maybeAction = this.preCommitAction(action); + if (!maybeAction) { + return observableOf(action); + } else if (isAction(maybeAction)) { + return observableOf(maybeAction); + } else { + return maybeAction; + } + }), + concatMap(action => { + return concat( + this.commitSingleAction(action).pipe(ignoreElements()), + observableOf(action), + ); + }), + concatMap(action => this.postCommitAction(action) || observableOf(null)), + ), + deferObservable(() => this._done()), + deferObservable(() => this.postCommit() || observableOf(null)), + ).pipe(ignoreElements()); } } diff --git a/packages/angular_devkit/schematics/src/tree/action.ts b/packages/angular_devkit/schematics/src/tree/action.ts index 1a0ed47421..5413a82ed5 100644 --- a/packages/angular_devkit/schematics/src/tree/action.ts +++ b/packages/angular_devkit/schematics/src/tree/action.ts @@ -53,56 +53,76 @@ export class ActionList implements Iterable { optimize() { - let changed = false; - const actions = this._actions; - const deleted = new Set(); - this._actions = []; + const toCreate = new Map(); + const toRename = new Map(); + const toOverwrite = new Map(); + const toDelete = new Set(); + + for (const action of this._actions) { + switch (action.kind) { + case 'c': + toCreate.set(action.path, action.content); + break; + + case 'o': + if (toCreate.has(action.path)) { + toCreate.set(action.path, action.content); + } else { + toOverwrite.set(action.path, action.content); + } + break; + + case 'd': + toDelete.add(action.path); + break; + + case 'r': + const maybeCreate = toCreate.get(action.path); + const maybeOverwrite = toOverwrite.get(action.path); + if (maybeCreate) { + toCreate.delete(action.path); + toCreate.set(action.to, maybeCreate); + } + if (maybeOverwrite) { + toOverwrite.delete(action.path); + toOverwrite.set(action.to, maybeOverwrite); + } - // Handles files we create. - for (let i = 0; i < actions.length; i++) { - const iAction = actions[i]; - if (iAction.kind == 'c') { - let path = iAction.path; - let content = iAction.content; - let toDelete = false; - deleted.delete(path); - - for (let j = i + 1; j < actions.length; j++) { - const action = actions[j]; - if (path == action.path) { - changed = true; - switch (action.kind) { - case 'c': content = action.content; actions.splice(j--, 1); break; - case 'o': content = action.content; actions.splice(j--, 1); break; - case 'r': path = action.to; actions.splice(j--, 1); break; - case 'd': toDelete = true; actions.splice(j--, 1); break; + let maybeRename: Path | undefined = undefined; + for (const [from, to] of toRename.entries()) { + if (to == action.path) { + maybeRename = from; + break; } } - if (toDelete) { - break; + + if (maybeRename) { + toRename.set(maybeRename, action.to); + } + + if (!maybeCreate && !maybeOverwrite && !maybeRename) { + toRename.set(action.path, action.to); } - } - - if (!toDelete) { - this.create(path, content); - } else { - deleted.add(path); - } - } else if (deleted.has(iAction.path)) { - // DoNothing - } else { - switch (iAction.kind) { - case 'o': this.overwrite(iAction.path, iAction.content); break; - case 'r': this.rename(iAction.path, iAction.to); break; - case 'd': this.delete(iAction.path); break; - } + break; } } - // TODO: fix the optimization and remove this recursivity. - if (changed) { - this.optimize(); - } + this._actions = []; + toDelete.forEach(x => { + this.delete(x); + }); + + toRename.forEach((to, from) => { + this.rename(from, to); + }); + + toCreate.forEach((content, path) => { + this.create(path, content); + }); + + toOverwrite.forEach((content, path) => { + this.overwrite(path, content); + }); } push(action: Action) { this._actions.push(action); } diff --git a/packages/angular_devkit/schematics/src/tree/action_spec.ts b/packages/angular_devkit/schematics/src/tree/action_spec.ts index a1dd916d8f..63bc24f16e 100644 --- a/packages/angular_devkit/schematics/src/tree/action_spec.ts +++ b/packages/angular_devkit/schematics/src/tree/action_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { normalize } from '@angular-devkit/core'; -import { ActionList } from './action'; +import { Action, ActionList } from './action'; describe('Action', () => { @@ -75,5 +75,39 @@ describe('Action', () => { actions2.optimize(); expect(actions2.length).toBe(2); }); + + it('handles edge cases (2)', () => { + const actions = new ActionList; + + actions.create(normalize('/test'), new Buffer('1')); + actions.rename(normalize('/test'), normalize('/test1')); + actions.overwrite(normalize('/test1'), new Buffer('2')); + actions.rename(normalize('/test1'), normalize('/test2')); + + actions.optimize(); + expect(actions.length).toBe(1); + expect(actions.get(0)).toEqual( + jasmine.objectContaining({ kind: 'c', path: normalize('/test2') }), + ); + }); + + it('handles edge cases (3)', () => { + const actions = new ActionList; + + actions.rename(normalize('/test'), normalize('/test1')); + actions.overwrite(normalize('/test1'), new Buffer('2')); + actions.rename(normalize('/test1'), normalize('/test2')); + + actions.optimize(); + expect(actions.length).toBe(2); + expect(actions.get(0)).toEqual( + jasmine.objectContaining({ + kind: 'r', path: normalize('/test'), to: normalize('/test2'), + }), + ); + expect(actions.get(1)).toEqual( + jasmine.objectContaining({ kind: 'o', path: normalize('/test2') }), + ); + }); }); }); diff --git a/packages/angular_devkit/schematics/src/tree/filesystem.ts b/packages/angular_devkit/schematics/src/tree/filesystem.ts index 5f647259d0..056e5454c6 100644 --- a/packages/angular_devkit/schematics/src/tree/filesystem.ts +++ b/packages/angular_devkit/schematics/src/tree/filesystem.ts @@ -173,6 +173,9 @@ export class FileSystemTree extends VirtualTree { } +export class HostTree extends FileSystemTree {} + + export class FileSystemCreateTree extends FileSystemTree { constructor(host: virtualFs.Host) { super(host); diff --git a/packages/angular_devkit/schematics/src/workflow/index.ts b/packages/angular_devkit/schematics/src/workflow/index.ts new file mode 100644 index 0000000000..7a1287cfc6 --- /dev/null +++ b/packages/angular_devkit/schematics/src/workflow/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './interface'; diff --git a/packages/angular_devkit/schematics/src/workflow/interface.ts b/packages/angular_devkit/schematics/src/workflow/interface.ts new file mode 100644 index 0000000000..01de696707 --- /dev/null +++ b/packages/angular_devkit/schematics/src/workflow/interface.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; + +export interface RequiredWorkflowExecutionContext { + collection: string; + schematic: string; + options: object; +} + +export interface WorkflowExecutionContext extends RequiredWorkflowExecutionContext { + debug: boolean; + logger: logging.Logger; + parentContext?: Readonly; + allowPrivate?: boolean; +} + +export interface LifeCycleEvent { + kind: 'start' | 'end' // Start and end of the full workflow execution. + | 'workflow-start' | 'workflow-end' // Start and end of a workflow execution. Can be more. + | 'post-tasks-start' | 'post-tasks-end'; // Start and end of the post tasks execution. +} + +export interface Workflow { + readonly context: Readonly; + + execute( + options: Partial & RequiredWorkflowExecutionContext, + ): Observable; +} diff --git a/packages/angular_devkit/schematics/tasks/index.ts b/packages/angular_devkit/schematics/tasks/index.ts index a4bf01809e..880b695666 100644 --- a/packages/angular_devkit/schematics/tasks/index.ts +++ b/packages/angular_devkit/schematics/tasks/index.ts @@ -8,3 +8,5 @@ export { NodePackageInstallTask } from './node-package/install-task'; export { NodePackageLinkTask } from './node-package/link-task'; export { RepositoryInitializerTask } from './repo-init/init-task'; +export { RunSchematicTask } from './run-schematic/task'; +export { TslintFixTask } from './tslint-fix/task'; diff --git a/packages/angular_devkit/schematics/tasks/node-package/executor.ts b/packages/angular_devkit/schematics/tasks/node-package/executor.ts index 4f6af9f5fa..3b008f7972 100644 --- a/packages/angular_devkit/schematics/tasks/node-package/executor.ts +++ b/packages/angular_devkit/schematics/tasks/node-package/executor.ts @@ -8,7 +8,7 @@ import { BaseException } from '@angular-devkit/core'; import { SpawnOptions, spawn } from 'child_process'; import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { TaskExecutor } from '../../src'; import { NodePackageTaskFactoryOptions, NodePackageTaskOptions } from './options'; @@ -44,6 +44,16 @@ export default function( const rootDirectory = factoryOptions.rootDirectory || process.cwd(); return (options: NodePackageTaskOptions) => { + let taskPackageManagerProfile = packageManagerProfile; + let taskPackageManagerName = packageManagerName; + if (factoryOptions.allowPackageManagerOverride && options.packageManager) { + taskPackageManagerProfile = packageManagers[options.packageManager]; + if (!taskPackageManagerProfile) { + throw new UnknownPackageManagerException(options.packageManager); + } + taskPackageManagerName = options.packageManager; + } + const outputStream = process.stdout; const errorStream = process.stderr; const spawnOptions: SpawnOptions = { @@ -57,12 +67,12 @@ export default function( args.push(options.packageName); } - if (options.quiet && packageManagerProfile.quietArgument) { - args.push(packageManagerProfile.quietArgument); + if (options.quiet && taskPackageManagerProfile.quietArgument) { + args.push(taskPackageManagerProfile.quietArgument); } return new Observable(obs => { - spawn(packageManagerName, args, spawnOptions) + spawn(taskPackageManagerName, args, spawnOptions) .on('close', (code: number) => { if (code === 0) { obs.next(); diff --git a/packages/angular_devkit/schematics/tasks/node-package/install-task.ts b/packages/angular_devkit/schematics/tasks/node-package/install-task.ts index 60824eca87..21b1cf4142 100644 --- a/packages/angular_devkit/schematics/tasks/node-package/install-task.ts +++ b/packages/angular_devkit/schematics/tasks/node-package/install-task.ts @@ -8,10 +8,39 @@ import { TaskConfiguration, TaskConfigurationGenerator } from '../../src'; import { NodePackageName, NodePackageTaskOptions } from './options'; +export class NodePackageInstallTaskOptions { + packageManager: string; + packageName: string; + workingDirectory: string; + quiet: boolean; +} + export class NodePackageInstallTask implements TaskConfigurationGenerator { quiet = true; + workingDirectory?: string; + packageManager?: string; + packageName?: string; - constructor(public workingDirectory?: string) {} + constructor(workingDirectory?: string); + constructor(options: Partial); + constructor(options?: string | Partial) { + if (typeof options === 'string') { + this.workingDirectory = options; + } else if (typeof options === 'object') { + if (options.quiet != undefined) { + this.quiet = options.quiet; + } + if (options.workingDirectory != undefined) { + this.workingDirectory = options.workingDirectory; + } + if (options.packageManager != undefined) { + this.packageManager = options.packageManager; + } + if (options.packageName != undefined) { + this.packageName = options.packageName; + } + } + } toConfiguration(): TaskConfiguration { return { @@ -20,6 +49,8 @@ export class NodePackageInstallTask implements TaskConfigurationGenerator = { @@ -22,4 +24,12 @@ export class BuiltinTaskExecutor { name: RepositoryInitializerName, create: (options) => import('../repo-init/executor').then(mod => mod.default(options)), }; + static readonly RunSchematic: TaskExecutorFactory<{}> = { + name: RunSchematicName, + create: () => import('../run-schematic/executor').then(mod => mod.default()), + }; + static readonly TslintFix: TaskExecutorFactory<{}> = { + name: TslintFixName, + create: () => import('../tslint-fix/executor').then(mod => mod.default()), + }; } diff --git a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts index 697661fc48..607abd5e06 100644 --- a/packages/angular_devkit/schematics/tasks/repo-init/executor.ts +++ b/packages/angular_devkit/schematics/tasks/repo-init/executor.ts @@ -32,10 +32,15 @@ export default function( shell: true, cwd: path.join(rootDirectory, options.workingDirectory || ''), env: { - GIT_AUTHOR_NAME: authorName, - GIT_COMMITTER_NAME: authorName, - GIT_AUTHOR_EMAIL: authorEmail, - GIT_COMMITTER_EMAIL: authorEmail, + ...process.env, + ...(authorName + ? { GIT_AUTHOR_NAME: authorName, GIT_COMMITTER_NAME: authorName } + : {} + ), + ...(authorEmail + ? { GIT_AUTHOR_EMAIL: authorEmail, GIT_COMMITTER_EMAIL: authorEmail } + : {} + ), }, }; diff --git a/packages/angular_devkit/schematics/tasks/repo-init/init-task.ts b/packages/angular_devkit/schematics/tasks/repo-init/init-task.ts index c24e58412e..df2a2888d4 100644 --- a/packages/angular_devkit/schematics/tasks/repo-init/init-task.ts +++ b/packages/angular_devkit/schematics/tasks/repo-init/init-task.ts @@ -10,8 +10,8 @@ import { RepositoryInitializerName, RepositoryInitializerTaskOptions } from './o export interface CommitOptions { message?: string; - name: string; - email: string; + name?: string; + email?: string; } export class RepositoryInitializerTask diff --git a/packages/angular_devkit/schematics/tasks/run-schematic/executor.ts b/packages/angular_devkit/schematics/tasks/run-schematic/executor.ts new file mode 100644 index 0000000000..dd7e144c68 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/run-schematic/executor.ts @@ -0,0 +1,29 @@ +/** + * @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 { SchematicContext, TaskExecutor } from '../../src'; +import { RunSchematicTaskOptions } from './options'; + + +export default function(): TaskExecutor> { + return (options: RunSchematicTaskOptions<{}>, context: SchematicContext) => { + const maybeWorkflow = context.engine.workflow; + const collection = options.collection || context.schematic.collection.description.name; + + if (!maybeWorkflow) { + throw new Error('Need Workflow to support executing schematics as post tasks.'); + } + + return maybeWorkflow.execute({ + collection: collection, + schematic: options.name, + options: options.options, + // Allow private when calling from the same collection. + allowPrivate: collection == context.schematic.collection.description.name, + }); + }; +} diff --git a/packages/angular_devkit/schematics/tasks/run-schematic/options.ts b/packages/angular_devkit/schematics/tasks/run-schematic/options.ts new file mode 100644 index 0000000000..a7d90f6327 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/run-schematic/options.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export const RunSchematicName = 'run-schematic'; + +export interface RunSchematicTaskOptions { + collection: string | null; + name: string; + options: T; +} diff --git a/packages/angular_devkit/schematics/tasks/run-schematic/task.ts b/packages/angular_devkit/schematics/tasks/run-schematic/task.ts new file mode 100644 index 0000000000..b8f17d3078 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/run-schematic/task.ts @@ -0,0 +1,42 @@ +/** + * @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 { TaskConfiguration, TaskConfigurationGenerator } from '../../src'; +import { RunSchematicName, RunSchematicTaskOptions } from './options'; + + +export class RunSchematicTask implements TaskConfigurationGenerator> { + protected _collection: string | null; + protected _schematic: string; + protected _options: T; + + constructor(s: string, o: T); + constructor(c: string, s: string, o: T); + + constructor(c: string | null, s: string | T, o?: T) { + if (arguments.length == 2 || typeof s !== 'string') { + o = s as T; + s = c as string; + c = null; + } + + this._collection = c; + this._schematic = s as string; + this._options = o as T; + } + + toConfiguration(): TaskConfiguration> { + return { + name: RunSchematicName, + options: { + collection: this._collection, + name: this._schematic, + options: this._options, + }, + }; + } +} diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts new file mode 100644 index 0000000000..856d3504d7 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/executor.ts @@ -0,0 +1,187 @@ +/** + * @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 { resolve } from '@angular-devkit/core/node'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { + Configuration as ConfigurationNS, + Linter as LinterNS, +} from 'tslint'; // tslint:disable-line:no-implicit-dependencies +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies +import { SchematicContext, TaskExecutor } from '../../src'; +import { TslintFixTaskOptions } from './options'; + + +type ConfigurationT = typeof ConfigurationNS; +type LinterT = typeof LinterNS; + + +function _loadConfiguration( + Configuration: ConfigurationT, + options: TslintFixTaskOptions, + root: string, + file?: string, +) { + if (options.tslintConfig) { + return Configuration.parseConfigFile(options.tslintConfig, root); + } else if (options.tslintPath) { + const tslintPath = path.join(root, options.tslintPath); + + return Configuration.findConfiguration(tslintPath, file && path.join(root, file)).results; + } else { + throw new Error('Executor must specify a tslint configuration.'); + } +} + + +function _getFileContent( + file: string, + options: TslintFixTaskOptions, + program?: ts.Program, +): string | undefined { + // The linter retrieves the SourceFile TS node directly if a program is used + if (program) { + const source = program.getSourceFile(file); + if (!source) { + const message + = `File '${file}' is not part of the TypeScript project '${options.tsConfigPath}'.`; + throw new Error(message); + } + + return source.getFullText(source); + } + + // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. + try { + // Strip BOM from file data. + // https://stackoverflow.com/questions/24356713 + return fs.readFileSync(file, 'utf-8').replace(/^\uFEFF/, ''); + } catch (e) { + throw new Error(`Could not read file '${file}'.`); + } +} + + +function _listAllFiles(root: string): string[] { + const result: string[] = []; + + function _recurse(location: string) { + const dir = fs.readdirSync(path.join(root, location)); + + dir.forEach(name => { + const loc = path.join(location, name); + if (fs.statSync(path.join(root, loc)).isDirectory()) { + _recurse(loc); + } else { + result.push(loc); + } + }); + } + _recurse(''); + + return result; +} + + +export default function(): TaskExecutor { + return (options: TslintFixTaskOptions, context: SchematicContext) => { + return new Observable(obs => { + const root = process.cwd(); + const tslint = require(resolve('tslint', { + basedir: root, + checkGlobal: true, + checkLocal: true, + })); + const includes = ( + Array.isArray(options.includes) + ? options.includes + : (options.includes ? [options.includes] : []) + ); + + const Linter = tslint.Linter as LinterT; + const Configuration = tslint.Configuration as ConfigurationT; + let program: ts.Program | undefined = undefined; + let filesToLint: string[] = []; + + if (options.tsConfigPath) { + const tsConfigPath = path.join(process.cwd(), options.tsConfigPath); + + if (!fs.existsSync(tsConfigPath)) { + obs.error(new Error('Could not find tsconfig.')); + + return; + } + program = Linter.createProgram(tsConfigPath); + filesToLint = Linter.getFileNames(program); + } + + if (includes.length > 0) { + const allFilesRel = _listAllFiles(root); + const pattern = '^(' + + (includes as string[]) + .map(ex => '(' + + ex.split(/[\/\\]/g).map(f => f + .replace(/[\-\[\]{}()+?.^$|]/g, '\\$&') + .replace(/^\*\*/g, '(.+?)?') + .replace(/\*/g, '[^/\\\\]*')) + .join('[\/\\\\]') + + ')') + .join('|') + + ')($|/|\\\\)'; + const re = new RegExp(pattern); + + filesToLint.push(...allFilesRel + .filter(x => re.test(x)) + .map(x => path.join(root, x)), + ); + } + + const lintOptions = { + fix: true, + formatter: options.format || 'prose', + }; + + const linter = new Linter(lintOptions, program); + const config = _loadConfiguration(Configuration, options, root); + + for (const file of filesToLint) { + const content = _getFileContent(file, options, program); + + if (!content) { + continue; + } + + linter.lint(file, content, config); + } + + const result = linter.getResult(); + + // Format and show the results. + if (!options.silent) { + const Formatter = tslint.findFormatter(options.format || 'prose'); + if (!Formatter) { + throw new Error(`Invalid lint format "${options.format}".`); + } + const formatter = new Formatter(); + + const output = formatter.format(result.failures, result.fixes); + if (output) { + context.logger.info(output); + } + } + + if (!options.ignoreErrors && result.errorCount > 0) { + obs.error(new Error('Lint errors were found.')); + } else { + obs.next(); + obs.complete(); + } + }); + }; +} diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/executor_spec.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/executor_spec.ts new file mode 100644 index 0000000000..467d953bb2 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/executor_spec.ts @@ -0,0 +1,134 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable:no-implicit-dependencies +import { getSystemPath, normalize, virtualFs } from '@angular-devkit/core'; +import { TempScopedNodeJsSyncHost } from '@angular-devkit/core/node/testing'; +import { HostTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Observable, concat } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +describe('TsLintTaskExecutor', () => { + + it('works with config object', done => { + const testRunner = new SchematicTestRunner( + '@_/test', + path.join(__dirname, 'test/collection.json'), + ); + + const host = new TempScopedNodeJsSyncHost(); + host.write(normalize('/file.ts'), virtualFs.stringToFileBuffer(` + export function() { console.log(1); } + `)).subscribe(); + const tree = new UnitTestTree(new HostTree(host)); + + testRunner.runSchematicAsync('run-task', null, tree) + .subscribe(undefined, done.fail, done); + }); + + it('shows errors with config object', done => { + const testRunner = new SchematicTestRunner( + '@_/test', + path.join(__dirname, 'test/collection.json'), + ); + + const host = new TempScopedNodeJsSyncHost(); + host.write(normalize('/file.ts'), virtualFs.stringToFileBuffer(` + // ${'...MORE_THAN_100'.repeat(10)} + export function() { console.log(1); } + `)).subscribe(); + const tree = new UnitTestTree(new HostTree(host)); + + const messages: string[] = []; + let error = false; + + concat( + testRunner.runSchematicAsync('run-task', null, tree), + new Observable(obs => { + process.chdir(getSystemPath(host.root)); + testRunner.logger.subscribe(x => messages.push(x.message)); + testRunner.engine.executePostTasks().subscribe(obs); + }).pipe( + catchError(() => { + error = true; + + return []; + }), + ), + new Observable(obs => { + expect(messages.find(msg => /\b80\b/.test(msg))).not.toBeUndefined(); + expect(error).toBe(true); + + obs.complete(); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('supports custom rules in the project (pass)', done => { + const testRunner = new SchematicTestRunner( + '@_/test', + path.join(__dirname, 'test/collection.json'), + ); + + const host = new TempScopedNodeJsSyncHost(); + host.write(normalize('/file.ts'), virtualFs.stringToFileBuffer(` + console.log('hello world'); + `)).subscribe(); + const tree = new UnitTestTree(new HostTree(host)); + + const messages: string[] = []; + + concat( + testRunner.runSchematicAsync('custom-rule', { shouldPass: true }, tree), + new Observable(obs => { + process.chdir(getSystemPath(host.root)); + testRunner.logger.subscribe(x => messages.push(x.message)); + testRunner.engine.executePostTasks().subscribe(obs); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('supports custom rules in the project (fail)', done => { + const testRunner = new SchematicTestRunner( + '@_/test', + path.join(__dirname, 'test/collection.json'), + ); + + const host = new TempScopedNodeJsSyncHost(); + host.write(normalize('/file.ts'), virtualFs.stringToFileBuffer(` + console.log('hello world'); + `)).subscribe(); + const tree = new UnitTestTree(new HostTree(host)); + + const messages: string[] = []; + let error = false; + + concat( + testRunner.runSchematicAsync('custom-rule', { shouldPass: false }, tree), + new Observable(obs => { + process.chdir(getSystemPath(host.root)); + testRunner.logger.subscribe(x => messages.push(x.message)); + testRunner.engine.executePostTasks().subscribe(obs); + }).pipe( + catchError(() => { + error = true; + + return []; + }), + ), + new Observable(obs => { + expect(messages.find(msg => /\bcustom-rule fail\b/.test(msg))).not.toBeUndefined(); + expect(error).toBe(true); + + obs.complete(); + }), + ).subscribe(undefined, done.fail, done); + }); + +}); diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts new file mode 100644 index 0000000000..e6b3ac520f --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/options.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '@angular-devkit/core'; + +export const TslintFixName = 'tslint-fix'; + +export interface TslintFixTaskOptionsBase { + silent?: boolean; + format?: string; + tsConfigPath?: string; + + ignoreErrors?: boolean; + + includes?: string | string[]; +} + +export interface TslintFixTaskOptionsPath extends TslintFixTaskOptionsBase { + tslintPath: string; + tslintConfig?: never; +} + +export interface TslintFixTaskOptionsConfig extends TslintFixTaskOptionsBase { + tslintPath?: never; + tslintConfig: JsonObject; +} + +export type TslintFixTaskOptions = TslintFixTaskOptionsPath | TslintFixTaskOptionsConfig; diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts new file mode 100644 index 0000000000..3b6eaa000b --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/task.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '@angular-devkit/core'; +import { TaskConfiguration, TaskConfigurationGenerator } from '../../src'; +import { TslintFixName, TslintFixTaskOptions, TslintFixTaskOptionsBase } from './options'; + + +export class TslintFixTask implements TaskConfigurationGenerator { + constructor(config: JsonObject, options: TslintFixTaskOptionsBase); + constructor(path: string, options: TslintFixTaskOptionsBase); + constructor( + protected _configOrPath: string | JsonObject, + protected _options: TslintFixTaskOptionsBase, + ) {} + + toConfiguration(): TaskConfiguration { + const options = { + ...this._options, + ...((typeof this._configOrPath == 'string' + ? { tslintPath: this._configOrPath } + : { tslintConfig: this._configOrPath })), + }; + + return { name: TslintFixName, options }; + } +} diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/test/collection.json b/packages/angular_devkit/schematics/tasks/tslint-fix/test/collection.json new file mode 100644 index 0000000000..128ff4793b --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/test/collection.json @@ -0,0 +1,12 @@ +{ + "schematics": { + "custom-rule": { + "description": ".", + "factory": "./custom-rule" + }, + "run-task": { + "description": ".", + "factory": "./run-task" + } + } +} diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/test/custom-rule.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/test/custom-rule.ts new file mode 100644 index 0000000000..d8c2339118 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/test/custom-rule.ts @@ -0,0 +1,30 @@ +/** + * @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 { + Rule, + SchematicContext, + Tree, +} from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies +import { + TslintFixTask, +} from '@angular-devkit/schematics/tasks'; // tslint:disable-line:no-implicit-dependencies +import * as path from 'path'; + +export default function(options: { shouldPass: boolean }): Rule { + return (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rulesDirectory: path.join(__dirname, 'rules'), + rules: { + 'custom-rule': [true, options.shouldPass], + }, + }, { + includes: '*.ts', + silent: false, + })); + }; +} diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/test/rules/customRuleRule.js b/packages/angular_devkit/schematics/tasks/tslint-fix/test/rules/customRuleRule.js new file mode 100644 index 0000000000..68c4882e0f --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/test/rules/customRuleRule.js @@ -0,0 +1,34 @@ +"use strict"; +/** + * @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 + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const Lint = require('tslint'); + + +class Rule extends Lint.Rules.AbstractRule { + apply(sourceFile) { + const shouldPass = this.getOptions().ruleArguments[0]; + if (!shouldPass) { + return [ new Lint.RuleFailure(sourceFile, 0, 0, 'custom-rule fail', this.ruleName) ]; + } else { + return []; + } + } +} + +Rule.metadata = { + ruleName: 'custom-rule', + description: 'Test.', + rationale: 'Do not use this.', + options: [{ type: 'boolean' }], + optionsDescription: '.', + type: 'functionality', + typescriptOnly: false, +}; + +exports.Rule = Rule; diff --git a/packages/angular_devkit/schematics/tasks/tslint-fix/test/run-task.ts b/packages/angular_devkit/schematics/tasks/tslint-fix/test/run-task.ts new file mode 100644 index 0000000000..571c08d978 --- /dev/null +++ b/packages/angular_devkit/schematics/tasks/tslint-fix/test/run-task.ts @@ -0,0 +1,28 @@ +/** + * @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 { + Rule, + SchematicContext, + Tree, +} from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies +import { + TslintFixTask, +} from '@angular-devkit/schematics/tasks'; // tslint:disable-line:no-implicit-dependencies + +export default function(): Rule { + return (_: Tree, context: SchematicContext) => { + context.addTask(new TslintFixTask({ + rules: { + 'max-line-length': [true, 80], + }, + }, { + includes: '*.ts', + silent: false, + })); + }; +} diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index 3c45024d29..c72458386b 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { logging, schema } from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; +import { Observable, of as observableOf } from 'rxjs'; import { map } from 'rxjs/operators'; import { Collection, @@ -16,6 +15,7 @@ import { Schematic, SchematicContext, SchematicEngine, + TaskConfiguration, Tree, VirtualTree, formats, @@ -61,19 +61,24 @@ export class SchematicTestRunner { this._engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.NodePackage); this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.RepositoryInitializer); + this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic); + this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.TslintFix); this._collection = this._engine.createCollection(this._collectionName); } + get engine() { return this._engine; } get logger(): logging.Logger { return this._logger; } + get tasks(): TaskConfiguration[] { return [...this._engineHost.tasks]; } runSchematicAsync( schematicName: string, opts?: SchematicSchemaT, tree?: Tree, ): Observable { - const schematic = this._collection.createSchematic(schematicName); + const schematic = this._collection.createSchematic(schematicName, true); const host = observableOf(tree || new VirtualTree); + this._engineHost.clearTasks(); return schematic.call(opts || {}, host, { logger: this._logger }) .pipe(map(tree => new UnitTestTree(tree))); @@ -84,10 +89,54 @@ export class SchematicTestRunner { opts?: SchematicSchemaT, tree?: Tree, ): UnitTestTree { - const schematic = this._collection.createSchematic(schematicName); + const schematic = this._collection.createSchematic(schematicName, true); let result: UnitTestTree | null = null; + let error; const host = observableOf(tree || new VirtualTree); + this._engineHost.clearTasks(); + + schematic.call(opts || {}, host, { logger: this._logger }) + .subscribe(t => result = new UnitTestTree(t), e => error = e); + + if (error) { + throw error; + } + + if (result === null) { + throw new Error('Schematic is async, please use runSchematicAsync'); + } + + return result; + } + + runExternalSchematicAsync( + collectionName: string, + schematicName: string, + opts?: SchematicSchemaT, + tree?: Tree, + ): Observable { + const externalCollection = this._engine.createCollection(collectionName); + const schematic = externalCollection.createSchematic(schematicName, true); + const host = observableOf(tree || new VirtualTree); + this._engineHost.clearTasks(); + + return schematic.call(opts || {}, host, { logger: this._logger }) + .pipe(map(tree => new UnitTestTree(tree))); + } + + runExternalSchematic( + collectionName: string, + schematicName: string, + opts?: SchematicSchemaT, + tree?: Tree, + ): UnitTestTree { + const externalCollection = this._engine.createCollection(collectionName); + const schematic = externalCollection.createSchematic(schematicName, true); + + let result: UnitTestTree | null = null; + const host = observableOf(tree || new VirtualTree); + this._engineHost.clearTasks(); schematic.call(opts || {}, host, { logger: this._logger }) .subscribe(t => result = new UnitTestTree(t)); diff --git a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts index 89f3dd891a..a9c327cbc7 100644 --- a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts @@ -5,10 +5,8 @@ * Use 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 { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; -import { _throw } from 'rxjs/observable/throw'; -import { mergeMap } from 'rxjs/operators/mergeMap'; +import { Observable, of as observableOf, throwError } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; import { Url } from 'url'; import { Collection, @@ -31,6 +29,8 @@ export type FallbackCollectionDescription = { export type FallbackSchematicDescription = { description: SchematicDescription<{}, {}>; }; +export type FallbackContext = + TypedSchematicContext; export declare type OptionTransform = ( schematic: SchematicDescription, options: T, @@ -85,7 +85,7 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { createSourceFromUrl( url: Url, - context: TypedSchematicContext, + context: FallbackContext, ): Source | null { return context.schematic.collection.description.host.createSourceFromUrl(url, context); } @@ -99,6 +99,16 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { ) as {} as Observable; } + transformContext(context: FallbackContext): FallbackContext { + let result = context; + + this._hosts.forEach(host => { + result = (host.transformContext(result) || result) as FallbackContext; + }); + + return result; + } + /** * @deprecated Use `listSchematicNames`. */ @@ -126,7 +136,7 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { } } - return _throw(new UnregisteredTaskException(name)); + return throwError(new UnregisteredTaskException(name)); } hasTaskExecutor(name: string): boolean { diff --git a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts index ce085e777c..284325eabc 100644 --- a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts +++ b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts @@ -5,14 +5,17 @@ * Use 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, JsonObject, normalize, virtualFs } from '@angular-devkit/core'; +import { + BaseException, + JsonObject, + isObservable, + normalize, + virtualFs, +} from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { dirname, isAbsolute, join, resolve } from 'path'; -import { Observable } from 'rxjs/Observable'; -import { from as observableFrom } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; -import { _throw } from 'rxjs/observable/throw'; -import { mergeMap } from 'rxjs/operators/mergeMap'; +import { Observable, from as observableFrom, of as observableOf, throwError } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; import { Url } from 'url'; import { EngineHost, @@ -34,11 +37,6 @@ import { import { readJsonFile } from './file-system-utility'; -declare const Symbol: Symbol & { - readonly observable: symbol; -}; - - export declare type OptionTransform = (schematic: FileSystemSchematicDescription, options: T) => Observable; @@ -268,7 +266,7 @@ export abstract class FileSystemEngineHostBase implements .pipe( ...this._transforms.map(tFn => mergeMap(opt => { const newOptions = tFn(schematic, opt); - if (Symbol.observable in newOptions) { + if (isObservable(newOptions)) { return newOptions; } else { return observableOf(newOptions); @@ -277,6 +275,10 @@ export abstract class FileSystemEngineHostBase implements )) as {} as Observable; } + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext { + return context; + } + getSchematicRuleFactory( schematic: FileSystemSchematicDesc, _collection: FileSystemCollectionDesc): RuleFactory { @@ -293,7 +295,7 @@ export abstract class FileSystemEngineHostBase implements return factory(); } - return _throw(new UnregisteredTaskException(name)); + return throwError(new UnregisteredTaskException(name)); } hasTaskExecutor(name: string): boolean { diff --git a/packages/angular_devkit/schematics/tools/file-system-engine-host_spec.ts b/packages/angular_devkit/schematics/tools/file-system-engine-host_spec.ts index b88c2e0bc6..7d21f4dbfe 100644 --- a/packages/angular_devkit/schematics/tools/file-system-engine-host_spec.ts +++ b/packages/angular_devkit/schematics/tools/file-system-engine-host_spec.ts @@ -11,7 +11,7 @@ import { normalize, virtualFs } from '@angular-devkit/core'; import { FileSystemTree, HostSink, SchematicEngine } from '@angular-devkit/schematics'; import { FileSystemEngineHost } from '@angular-devkit/schematics/tools'; import * as path from 'path'; -import { of as observableOf } from 'rxjs/observable/of'; +import { of as observableOf } from 'rxjs'; describe('FileSystemEngineHost', () => { diff --git a/packages/angular_devkit/schematics/tools/index.ts b/packages/angular_devkit/schematics/tools/index.ts index 71bb85ca75..02a352ac8c 100644 --- a/packages/angular_devkit/schematics/tools/index.ts +++ b/packages/angular_devkit/schematics/tools/index.ts @@ -9,6 +9,8 @@ export * from './description'; export * from './file-system-engine-host-base'; export * from './file-system-host'; +export * from './workflow/node-workflow'; + export { FileSystemEngineHost } from './file-system-engine-host'; export { NodeModulesEngineHost } from './node-module-engine-host'; export { NodeModulesTestEngineHost } from './node-modules-test-engine-host'; diff --git a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts index bc7e52fbec..e0dd10d578 100644 --- a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts @@ -42,6 +42,19 @@ export class NodeModulesEngineHost extends FileSystemEngineHostBase { if (name.startsWith('.') || name.startsWith('/')) { return resolvePath(basedir, name); } else { + // If it's a file inside a package, resolve the package then return the file... + if (name.split('/').length > (name[0] == '@' ? 2 : 1)) { + const rest = name.split('/'); + const packageName = rest.shift() + (name[0] == '@' ? '/' + rest.shift() : ''); + + return resolvePath(core.resolve(packageName, { + basedir, + checkLocal: true, + checkGlobal: true, + resolvePackageJson: true, + }), '..', ...rest); + } + return core.resolve(name, { basedir, checkLocal: true, @@ -51,19 +64,31 @@ export class NodeModulesEngineHost extends FileSystemEngineHostBase { } protected _resolveCollectionPath(name: string): string { - let packageJsonPath = this._resolvePackageJson(name, process.cwd()); - // If it's a file, use it as is. Otherwise append package.json to it. - if (!core.fs.isFile(packageJsonPath)) { - packageJsonPath = join(packageJsonPath, 'package.json'); + let collectionPath: string | undefined = undefined; + + if (name.split('/').length > (name[0] == '@' ? 2 : 1)) { + try { + collectionPath = this._resolvePath(name, process.cwd()); + } catch (_) { + } } - try { + if (!collectionPath) { + let packageJsonPath = this._resolvePackageJson(name, process.cwd()); + // If it's a file, use it as is. Otherwise append package.json to it. + if (!core.fs.isFile(packageJsonPath)) { + packageJsonPath = join(packageJsonPath, 'package.json'); + } + const pkgJsonSchematics = require(packageJsonPath)['schematics']; - if (pkgJsonSchematics) { - const resolvedPath = this._resolvePath(pkgJsonSchematics, dirname(packageJsonPath)); - readJsonFile(resolvedPath); + collectionPath = this._resolvePath(pkgJsonSchematics, dirname(packageJsonPath)); + } + + try { + if (collectionPath) { + readJsonFile(collectionPath); - return resolvedPath; + return collectionPath; } } catch (e) { } diff --git a/packages/angular_devkit/schematics/tools/node-modules-test-engine-host.ts b/packages/angular_devkit/schematics/tools/node-modules-test-engine-host.ts index 003c0b5b75..698ca0cc7f 100644 --- a/packages/angular_devkit/schematics/tools/node-modules-test-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/node-modules-test-engine-host.ts @@ -5,6 +5,8 @@ * Use 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 { TaskConfiguration, TaskConfigurationGenerator, TaskId } from '../src/engine/task'; +import { FileSystemSchematicContext } from './description'; import { NodeModulesEngineHost } from './node-module-engine-host'; @@ -14,11 +16,27 @@ import { NodeModulesEngineHost } from './node-module-engine-host'; */ export class NodeModulesTestEngineHost extends NodeModulesEngineHost { private _collections = new Map(); + private _tasks = [] as TaskConfiguration[]; + + get tasks() { return this._tasks; } + + clearTasks() { this._tasks = []; } registerCollection(name: string, path: string) { this._collections.set(name, path); } + transformContext(context: FileSystemSchematicContext): FileSystemSchematicContext { + const oldAddTask = context.addTask; + context.addTask = (task: TaskConfigurationGenerator<{}>, dependencies?: Array) => { + this._tasks.push(task.toConfiguration()); + + return oldAddTask.call(context, task, dependencies); + }; + + return context; + } + protected _resolveCollectionPath(name: string): string { const maybePath = this._collections.get(name); if (maybePath) { diff --git a/packages/angular_devkit/schematics/tools/schema-option-transform.ts b/packages/angular_devkit/schematics/tools/schema-option-transform.ts index 13a665bf48..2614cb32b3 100644 --- a/packages/angular_devkit/schematics/tools/schema-option-transform.ts +++ b/packages/angular_devkit/schematics/tools/schema-option-transform.ts @@ -5,15 +5,9 @@ * Use 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, - schema, -} from '@angular-devkit/core'; -import { Observable } from 'rxjs/Observable'; -import { of as observableOf } from 'rxjs/observable/of'; -import { first } from 'rxjs/operators/first'; -import { map } from 'rxjs/operators/map'; -import { mergeMap } from 'rxjs/operators/mergeMap'; +import { deepCopy, schema } from '@angular-devkit/core'; +import { Observable, of as observableOf } from 'rxjs'; +import { first, map, mergeMap } from 'rxjs/operators'; import { SchematicDescription } from '../src'; import { FileSystemCollectionDescription, FileSystemSchematicDescription } from './description'; @@ -21,36 +15,20 @@ export type SchematicDesc = SchematicDescription; -export class InvalidInputOptions extends BaseException { - // tslint:disable-next-line:no-any - constructor(options: any, errors: string[]) { - super(`Schematic input does not validate against the Schema: ${JSON.stringify(options)}\n` - + `Errors:\n ${errors.join('\n ')}`); +export class InvalidInputOptions extends schema.SchemaValidationException { + constructor(options: T, errors: schema.SchemaValidatorError[]) { + super( + errors, + `Schematic input does not validate against the Schema: ${JSON.stringify(options)}\nErrors:\n`, + ); } } - -// tslint:disable-next-line:no-any -function _deepCopy(object: T): T { - const copy = {} as T; - for (const key of Object.keys(object)) { - if (typeof object[key] == 'object') { - copy[key] = _deepCopy(object[key]); - break; - } else { - copy[key] = object[key]; - } - } - - return copy; -} - - // This can only be used in NodeJS. export function validateOptionsWithSchema(registry: schema.SchemaRegistry) { return (schematic: SchematicDesc, options: T): Observable => { // Prevent a schematic from changing the options object by making a copy of it. - options = _deepCopy(options); + options = deepCopy(options); if (schematic.schema && schematic.schemaJson) { // Make a deep copy of options. @@ -61,7 +39,7 @@ export function validateOptionsWithSchema(registry: schema.SchemaRegistry) { first(), map(result => { if (!result.success) { - throw new InvalidInputOptions(options, result.errors || ['Unknown reason.']); + throw new InvalidInputOptions(options, result.errors || []); } return options; diff --git a/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts new file mode 100644 index 0000000000..c7b6ea5e66 --- /dev/null +++ b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts @@ -0,0 +1,183 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, logging, schema, virtualFs } from '@angular-devkit/core'; +import { + DryRunSink, + HostSink, + HostTree, + SchematicEngine, + Tree, + UnsuccessfulWorkflowExecution, + formats, + workflow, +} from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies +import { EMPTY, Observable, Subject, concat, of, throwError } from 'rxjs'; +import { reduce, tap } from 'rxjs/internal/operators'; +import { concatMap, ignoreElements, map } from 'rxjs/operators'; +import { NodeModulesEngineHost, validateOptionsWithSchema } from '..'; +import { DryRunEvent } from '../../src/sink/dryrun'; +import { BuiltinTaskExecutor } from '../../tasks/node'; + +export class NodeWorkflow implements workflow.Workflow { + protected _engine: SchematicEngine<{}, {}>; + protected _engineHost: NodeModulesEngineHost; + protected _registry: schema.CoreSchemaRegistry; + + protected _reporter: Subject = new Subject(); + protected _lifeCycle: Subject = new Subject(); + + protected _context: workflow.WorkflowExecutionContext[]; + + constructor( + protected _host: virtualFs.Host, + protected _options: { + force?: boolean; + dryRun?: boolean; + root?: Path, + packageManager?: string; + }, + ) { + /** + * Create the SchematicEngine, which is used by the Schematic library as callbacks to load a + * Collection or a Schematic. + */ + this._engineHost = new NodeModulesEngineHost(); + this._engine = new SchematicEngine(this._engineHost, this); + + // Add support for schemaJson. + this._registry = new schema.CoreSchemaRegistry(formats.standardFormats); + this._engineHost.registerOptionsTransform(validateOptionsWithSchema(this._registry)); + + this._engineHost.registerTaskExecutor( + BuiltinTaskExecutor.NodePackage, + { + allowPackageManagerOverride: true, + packageManager: this._options.packageManager, + rootDirectory: this._options.root, + }, + ); + this._engineHost.registerTaskExecutor( + BuiltinTaskExecutor.RepositoryInitializer, + { + rootDirectory: this._options.root, + }, + ); + this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic); + this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.TslintFix); + + this._context = []; + } + + get context(): Readonly { + const maybeContext = this._context[this._context.length - 1]; + if (!maybeContext) { + throw new Error('Cannot get context when workflow is not executing...'); + } + + return maybeContext; + } + get registry(): schema.SchemaRegistry { + return this._registry; + } + get reporter(): Observable { + return this._reporter.asObservable(); + } + get lifeCycle(): Observable { + return this._lifeCycle.asObservable(); + } + + execute( + options: Partial & workflow.RequiredWorkflowExecutionContext, + ): Observable { + const parentContext = this._context[this._context.length - 1]; + + if (!parentContext) { + this._lifeCycle.next({ kind: 'start' }); + } + + /** Create the collection and the schematic. */ + const collection = this._engine.createCollection(options.collection); + // Only allow private schematics if called from the same collection. + const allowPrivate = options.allowPrivate + || (parentContext && parentContext.collection === options.collection); + const schematic = collection.createSchematic(options.schematic, allowPrivate); + + // We need two sinks if we want to output what will happen, and actually do the work. + // Note that fsSink is technically not used if `--dry-run` is passed, but creating the Sink + // does not have any side effect. + const dryRunSink = new DryRunSink(this._host, this._options.force); + const fsSink = new HostSink(this._host, this._options.force); + + let error = false; + const dryRunSubscriber = dryRunSink.reporter.subscribe(event => { + this._reporter.next(event); + error = error || (event.kind == 'error'); + }); + + this._lifeCycle.next({ kind: 'workflow-start' }); + + const context = { + ...options, + debug: options.debug || false, + logger: options.logger || (parentContext && parentContext.logger) || new logging.NullLogger(), + parentContext, + }; + this._context.push(context); + + return concat( + schematic.call(options.options, of(new HostTree(this._host)), { + logger: context.logger, + }).pipe( + map(tree => Tree.optimize(tree)), + concatMap((tree: Tree) => { + return concat( + dryRunSink.commit(tree).pipe( + ignoreElements(), + ), + of(tree), + ); + }), + concatMap((tree: Tree) => { + dryRunSubscriber.unsubscribe(); + if (error) { + return throwError(new UnsuccessfulWorkflowExecution()); + } + if (this._options.dryRun) { + return EMPTY; + } + + return fsSink.commit(tree); + }), + ), + concat(new Observable(obs => { + if (!this._options.dryRun) { + this._lifeCycle.next({ kind: 'post-tasks-start' }); + this._engine.executePostTasks() + .pipe( + reduce(() => {}), + tap(() => this._lifeCycle.next({ kind: 'post-tasks-end' })), + ) + .subscribe(obs); + } else { + obs.complete(); + } + })), + concat(new Observable(obs => { + this._lifeCycle.next({ kind: 'workflow-end' }); + this._context.pop(); + + if (this._context.length == 0) { + this._lifeCycle.next({ kind: 'end' }); + } + + obs.complete(); + })), + reduce(() => {}), + ); + } +} diff --git a/packages/angular_devkit/schematics_cli/BUILD b/packages/angular_devkit/schematics_cli/BUILD index 16fa1280cf..31d6b8049b 100644 --- a/packages/angular_devkit/schematics_cli/BUILD +++ b/packages/angular_devkit/schematics_cli/BUILD @@ -14,6 +14,7 @@ ts_library( include = ["bin/**/*.ts"], exclude = [ "bin/**/*_spec.ts", + "bin/**/*_spec_large.ts", "bin/**/*_benchmark.ts", ], ), diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index 12ade640fd..6c44fc979d 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -6,36 +6,22 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ + +import 'symbol-observable'; +// symbol polyfill must go first +// tslint:disable-next-line:ordered-imports import-groups import { + JsonObject, normalize, - schema, tags, terminal, virtualFs, } from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; -import { - DryRunEvent, - DryRunSink, - FileSystemTree, - HostSink, - SchematicEngine, - Tree, - formats, -} from '@angular-devkit/schematics'; -import { BuiltinTaskExecutor } from '@angular-devkit/schematics/tasks/node'; -import { - NodeModulesEngineHost, - validateOptionsWithSchema, -} from '@angular-devkit/schematics/tools'; +import { DryRunEvent, UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics'; +import { NodeWorkflow } from '@angular-devkit/schematics/tools'; import * as minimist from 'minimist'; -import { of as observableOf } from 'rxjs/observable/of'; -import { - concat, - concatMap, - ignoreElements, - map, -} from 'rxjs/operators'; + /** * Show usage of the CLI tool, and exit the process. @@ -50,6 +36,8 @@ function usage(exitCode = 0): never { Options: --debug Debug mode. This is true by default if the collection is a relative path (in that case, turn off with --debug=false). + --allowPrivate Allow private schematics to be run from the command line. Default to + false. --dry-run Do not output anything, but instead just show what actions would be performed. Default to true if debug is also true. --force Force overwriting files that would otherwise be an error. @@ -101,7 +89,15 @@ function parseSchematicName(str: string | null): { collection: string, schematic /** Parse the command line. */ -const booleanArgs = [ 'debug', 'dry-run', 'force', 'help', 'list-schematics', 'verbose' ]; +const booleanArgs = [ + 'allowPrivate', + 'debug', + 'dry-run', + 'force', + 'help', + 'list-schematics', + 'verbose', +]; const argv = minimist(process.argv.slice(2), { boolean: booleanArgs, default: { @@ -126,81 +122,54 @@ const { const isLocalCollection = collectionName.startsWith('.') || collectionName.startsWith('/'); -/** - * Create the SchematicEngine, which is used by the Schematic library as callbacks to load a - * Collection or a Schematic. - */ -const engineHost = new NodeModulesEngineHost(); -const engine = new SchematicEngine(engineHost); - - -// Add support for schemaJson. -const registry = new schema.CoreSchemaRegistry(formats.standardFormats); -engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); - -engineHost.registerTaskExecutor(BuiltinTaskExecutor.NodePackage); -engineHost.registerTaskExecutor(BuiltinTaskExecutor.RepositoryInitializer); - -/** - * The collection to be used. - * @type {Collection|any} - */ -const collection = engine.createCollection(collectionName); -if (collection === null) { - logger.fatal(`Invalid collection name: "${collectionName}".`); - process.exit(3); - throw 3; // TypeScript doesn't know that process.exit() never returns. -} - - /** If the user wants to list schematics, we simply show all the schematic names. */ if (argv['list-schematics']) { - logger.info(engine.listSchematicNames(collection).join('\n')); + // logger.info(engine.listSchematicNames(collection).join('\n')); process.exit(0); throw 0; // TypeScript doesn't know that process.exit() never returns. } -/** Create the schematic from the collection. */ -const schematic = collection.createSchematic(schematicName); - /** Gather the arguments for later use. */ const debug: boolean = argv.debug === null ? isLocalCollection : argv.debug; const dryRun: boolean = argv['dry-run'] === null ? debug : argv['dry-run']; const force = argv['force']; +const allowPrivate = argv['allowPrivate']; /** Create a Virtual FS Host scoped to where the process is being run. **/ const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(process.cwd())); -/** This host is the original Tree created from the current directory. */ -const host = observableOf(new FileSystemTree(fsHost)); - -// We need two sinks if we want to output what will happen, and actually do the work. -// Note that fsSink is technically not used if `--dry-run` is passed, but creating the Sink -// does not have any side effect. -const dryRunSink = new DryRunSink(fsHost, force); -const fsSink = new HostSink(fsHost, force); - +/** Create the workflow that will be executed with this run. */ +const workflow = new NodeWorkflow(fsHost, { force, dryRun }); -// We keep a boolean to tell us whether an error would occur if we were to commit to an -// actual filesystem. In this case we simply show the dry-run, but skip the fsSink commit. -let error = false; - -// Indicate to the user when nothing has been done. +// Indicate to the user when nothing has been done. This is automatically set to off when there's +// a new DryRunEvent. let nothingDone = true; +// Logging queue that receives all the messages to show the users. This only get shown when no +// errors happened. +let loggingQueue: string[] = []; +let error = false; -const loggingQueue: string[] = []; - -// Logs out dry run events. -dryRunSink.reporter.subscribe((event: DryRunEvent) => { +/** + * Logs out dry run events. + * + * All events will always be executed here, in order of discovery. That means that an error would + * be shown along other events when it happens. Since errors in workflows will stop the Observable + * from completing successfully, we record any events other than errors, then on completion we + * show them. + * + * This is a simple way to only show errors when an error occur. + */ +workflow.reporter.subscribe((event: DryRunEvent) => { nothingDone = false; switch (event.kind) { case 'error': + error = true; + const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.'; logger.warn(`ERROR! ${event.path} ${desc}.`); - error = true; break; case 'update': loggingQueue.push(tags.oneLine` @@ -222,6 +191,22 @@ dryRunSink.reporter.subscribe((event: DryRunEvent) => { }); +/** + * Listen to lifecycle events of the workflow to flush the logs between each phases. + */ +workflow.lifeCycle.subscribe(event => { + if (event.kind == 'workflow-end' || event.kind == 'post-tasks-start') { + if (!error) { + // Flush the log queue and clean the error state. + loggingQueue.forEach(log => logger.info(log)); + } + + loggingQueue = []; + error = false; + } +}); + + /** * Remove every options from argv that we support in schematics itself. */ @@ -238,55 +223,51 @@ const argv2 = minimist(argv['--']); for (const key of Object.keys(argv2)) { args[key] = argv2[key]; } + +// Pass the rest of the arguments as the smart default "argv". Then delete it. +workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { + if ('index' in schema) { + return argv._[Number(schema['index'])]; + } else { + return argv._; + } +}); delete args._; /** - * The main path. Call the schematic with the host. This creates a new Context for the schematic - * to run in, then call the schematic rule using the input Tree. This returns a new Tree as if - * the schematic was applied to it. + * Execute the workflow, which will report the dry run events, run the tasks, and complete + * after all is done. * - * We then optimize this tree. This removes any duplicated actions or actions that would result - * in a noop (for example, creating then deleting a file). This is not necessary but will greatly - * improve performance as hitting the file system is costly. - * - * Then we proceed to run the dryRun commit. We run this before we then commit to the filesystem - * (if --dry-run was not passed or an error was detected by dryRun). + * The Observable returned will properly cancel the workflow if unsubscribed, error out if ANY + * step of the workflow failed (sink or task), with details included, and will only complete + * when everything is done. */ -schematic.call(args, host, { debug, logger: logger.asApi() }) - .pipe( - map((tree: Tree) => Tree.optimize(tree)), - concatMap((tree: Tree) => { - return dryRunSink.commit(tree).pipe( - ignoreElements(), - concat(observableOf(tree))); - }), - concatMap((tree: Tree) => { - if (!error) { - // Output the logging queue. - loggingQueue.forEach(log => logger.info(log)); - } - - if (nothingDone) { - logger.info('Nothing to be done.'); - } - - if (dryRun || error) { - return observableOf(tree); - } - - return fsSink.commit(tree).pipe( - ignoreElements(), - concat(observableOf(tree))); - }), - concatMap(() => engine.executePostTasks())) - .subscribe({ - error(err: Error) { - if (debug) { - logger.fatal('An error occured:\n' + err.stack); - } else { - logger.fatal(err.message); - } - process.exit(1); - }, - }); +workflow.execute({ + collection: collectionName, + schematic: schematicName, + options: args, + allowPrivate: allowPrivate, + debug: debug, + logger: logger, +}) +.subscribe({ + error(err: Error) { + // In case the workflow was not successful, show an appropriate error message. + if (err instanceof UnsuccessfulWorkflowExecution) { + // "See above" because we already printed the error. + logger.fatal('The Schematic workflow failed. See above.'); + } else if (debug) { + logger.fatal('An error occured:\n' + err.stack); + } else { + logger.fatal(err.message); + } + + process.exit(1); + }, + complete() { + if (nothingDone) { + logger.info('Nothing to be done.'); + } + }, +}); diff --git a/packages/angular_devkit/schematics_cli/package.json b/packages/angular_devkit/schematics_cli/package.json index 415b74c40f..6aaa5c332f 100644 --- a/packages/angular_devkit/schematics_cli/package.json +++ b/packages/angular_devkit/schematics_cli/package.json @@ -25,6 +25,7 @@ "@angular-devkit/schematics": "0.0.0", "@schematics/schematics": "0.0.0", "minimist": "^1.2.0", - "rxjs": "^5.5.6" + "symbol-observable": "^1.2.0", + "rxjs": "^6.0.0-beta.3" } } diff --git a/packages/ngtools/webpack/README.md b/packages/ngtools/webpack/README.md new file mode 100644 index 0000000000..de62be1d00 --- /dev/null +++ b/packages/ngtools/webpack/README.md @@ -0,0 +1,57 @@ +# Angular Ahead-of-Time Webpack Plugin + +Webpack 4.0 plugin that AoT compiles your Angular components and modules. + +## Usage + +In your webpack config, add the following plugin and loader. + +Angular version 5 and up, use `AngularCompilerPlugin`: + +```typescript +import {AngularCompilerPlugin} from '@ngtools/webpack' + +exports = { /* ... */ + module: { + rules: [ + { + test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, + loader: '@ngtools/webpack' + } + ] + }, + + plugins: [ + new AngularCompilerPlugin({ + tsConfigPath: 'path/to/tsconfig.json', + entryModule: 'path/to/app.module#AppModule', + sourceMap: true + }) + ] +} +``` + +The loader works with webpack plugin to compile your TypeScript. It's important to include both, and to not include any other TypeScript compiler loader. + +## Options + +* `tsConfigPath`. The path to the `tsconfig.json` file. This is required. In your `tsconfig.json`, you can pass options to the Angular Compiler with `angularCompilerOptions`. +* `basePath`. Optional. The root to use by the compiler to resolve file paths. By default, use the `tsConfigPath` root. +* `entryModule`. Optional if specified in `angularCompilerOptions`. The path and classname of the main application module. This follows the format `path/to/file#ClassName`. +* `mainPath`. Optional if `entryModule` is specified. The `main.ts` file containing the bootstrap code. The plugin will use AST to determine the `entryModule`. +* `skipCodeGeneration`. Optional, defaults to false. Disable code generation and do not refactor the code to bootstrap. This replaces `templateUrl: "string"` with `template: require("string")` (and similar for styles) to allow for webpack to properly link the resources. +* `sourceMap`. Optional. Include sourcemaps. +* `compilerOptions`. Optional. Override options in `tsconfig.json`. + +## Features +The benefits and ability of using [`@ngtools/webpack`](https://www.npmjs.com/~ngtools) standalone from the Angular CLI as presented in [Stephen Fluin's Angular CLI talk](https://youtu.be/uBRK6cTr4Vk?t=6m45s) at Angular Connect 2016: + +* Compiles SCSS/LESS +* TypeScript transpilation +* Bundles JavaScript, CSS +* Asset optimization +* Virtual filesystem for assets + * For serving local assets and compile versions. +* Live-reload via websockets +* Code splitting + * Recognizing the use of `loadChildren` in the router, and bundling those modules separately so that any dependencies of those modules are not going to be loaded as part of your main bundle. These separate bundles will be pulled out of the critical path of your application, making your total application bundle much smaller and loading it much more performant. diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json new file mode 100644 index 0000000000..da193a9e50 --- /dev/null +++ b/packages/ngtools/webpack/package.json @@ -0,0 +1,36 @@ +{ + "name": "@ngtools/webpack", + "version": "0.0.0", + "description": "Webpack plugin that AoT compiles your Angular components and modules.", + "main": "./src/index.js", + "typings": "src/index.d.ts", + "license": "MIT", + "keywords": [ + "angular", + "webpack", + "plugin", + "aot" + ], + "repository": { + "type": "git", + "url": "https://github.com/angular/angular-cli.git" + }, + "author": "angular", + "bugs": { + "url": "https://github.com/angular/angular-cli/issues" + }, + "homepage": "https://github.com/angular/angular-cli/tree/master/packages/@ngtools/webpack", + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + }, + "dependencies": { + "@angular-devkit/core": "0.0.0", + "tree-kill": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "peerDependencies": { + "typescript": "~2.5.0 || ~2.6.0 || ~2.7.0", + "webpack": "^4.0.0" + } +} diff --git a/packages/ngtools/webpack/src/angular_compiler_plugin.ts b/packages/ngtools/webpack/src/angular_compiler_plugin.ts new file mode 100644 index 0000000000..58318ccf4d --- /dev/null +++ b/packages/ngtools/webpack/src/angular_compiler_plugin.ts @@ -0,0 +1,1119 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// TODO: fix webpack typings. +// tslint:disable-next-line:no-global-tslint-disable +// tslint:disable:no-any +import { dirname, normalize, resolve, virtualFs } from '@angular-devkit/core'; +import { ChildProcess, ForkOptions, fork } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { time, timeEnd } from './benchmark'; +import { WebpackCompilerHost, workaroundResolve } from './compiler_host'; +import { resolveEntryModuleFromMain } from './entry_resolver'; +import { gatherDiagnostics, hasErrors } from './gather_diagnostics'; +import { LazyRouteMap, findLazyRoutes } from './lazy_routes'; +import { + CompilerCliIsSupported, + CompilerHost, + CompilerOptions, + DEFAULT_ERROR_CODE, + Diagnostic, + EmitFlags, + Program, + SOURCE, + UNKNOWN_ERROR_CODE, + VERSION, + __NGTOOLS_PRIVATE_API_2, + createCompilerHost, + createProgram, + formatDiagnostics, + readConfiguration, +} from './ngtools_api'; +import { resolveWithPaths } from './paths-plugin'; +import { WebpackResourceLoader } from './resource_loader'; +import { + exportLazyModuleMap, + exportNgFactory, + findResources, + registerLocaleData, + removeDecorators, + replaceBootstrap, + replaceResources, + replaceServerBootstrap, +} from './transformers'; +import { collectDeepNodes } from './transformers/ast_helpers'; +import { AUTO_START_ARG, InitMessage, UpdateMessage } from './type_checker'; +import { + VirtualFileSystemDecorator, + VirtualWatchFileSystemDecorator, +} from './virtual_file_system_decorator'; + + +const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); +const treeKill = require('tree-kill'); + + +/** + * Option Constants + */ +export interface AngularCompilerPluginOptions { + sourceMap?: boolean; + tsConfigPath: string; + basePath?: string; + entryModule?: string; + mainPath?: string; + skipCodeGeneration?: boolean; + hostReplacementPaths?: { [path: string]: string }; + forkTypeChecker?: boolean; + // TODO: remove singleFileIncludes for 2.0, this is just to support old projects that did not + // include 'polyfills.ts' in `tsconfig.spec.json'. + singleFileIncludes?: string[]; + i18nInFile?: string; + i18nInFormat?: string; + i18nOutFile?: string; + i18nOutFormat?: string; + locale?: string; + missingTranslation?: string; + platform?: PLATFORM; + nameLazyFiles?: boolean; + + // added to the list of lazy routes + additionalLazyModules?: { [module: string]: string }; + + // Use tsconfig to include path globs. + compilerOptions?: ts.CompilerOptions; + + host: virtualFs.Host; +} + +export enum PLATFORM { + Browser, + Server, +} + +export class AngularCompilerPlugin { + private _options: AngularCompilerPluginOptions; + + // TS compilation. + private _compilerOptions: CompilerOptions; + private _rootNames: string[]; + private _singleFileIncludes: string[] = []; + private _program: (ts.Program | Program) | null; + private _compilerHost: WebpackCompilerHost & CompilerHost; + private _moduleResolutionCache: ts.ModuleResolutionCache; + private _resourceLoader: WebpackResourceLoader; + // Contains `moduleImportPath#exportName` => `fullModulePath`. + private _lazyRoutes: LazyRouteMap = Object.create(null); + private _tsConfigPath: string; + private _entryModule: string | null; + private _mainPath: string | undefined; + private _basePath: string; + private _transformers: ts.TransformerFactory[] = []; + private _platform: PLATFORM; + private _JitMode = false; + private _emitSkipped = true; + private _changedFileExtensions = new Set(['ts', 'html', 'css']); + + // Webpack plugin. + private _firstRun = true; + private _donePromise: Promise | null; + private _normalizedLocale: string | null; + private _warnings: (string | Error)[] = []; + private _errors: (string | Error)[] = []; + + // TypeChecker process. + private _forkTypeChecker = true; + private _typeCheckerProcess: ChildProcess | null; + private _forkedTypeCheckerInitialized = false; + + private get _ngCompilerSupportsNewApi() { + if (this._JitMode) { + return false; + } else { + return !!(this._program as Program).listLazyRoutes; + } + } + + constructor(options: AngularCompilerPluginOptions) { + CompilerCliIsSupported(); + this._options = Object.assign({}, options); + this._setupOptions(this._options); + } + + get options() { return this._options; } + get done() { return this._donePromise; } + get entryModule() { + if (!this._entryModule) { + return null; + } + const splitted = this._entryModule.split(/(#[a-zA-Z_]([\w]+))$/); + const path = splitted[0]; + const className = !!splitted[1] ? splitted[1].substring(1) : 'default'; + + return { path, className }; + } + + static isSupported() { + return VERSION && parseInt(VERSION.major) >= 5; + } + + private _setupOptions(options: AngularCompilerPluginOptions) { + time('AngularCompilerPlugin._setupOptions'); + // Fill in the missing options. + if (!options.hasOwnProperty('tsConfigPath')) { + throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); + } + // TS represents paths internally with '/' and expects the tsconfig path to be in this format + this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/'); + + // Check the base path. + const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); + let basePath = maybeBasePath; + if (fs.statSync(maybeBasePath).isFile()) { + basePath = path.dirname(basePath); + } + if (options.basePath !== undefined) { + basePath = path.resolve(process.cwd(), options.basePath); + } + + if (options.singleFileIncludes !== undefined) { + this._singleFileIncludes.push(...options.singleFileIncludes); + } + + // Parse the tsconfig contents. + const config = readConfiguration(this._tsConfigPath); + if (config.errors && config.errors.length) { + throw new Error(formatDiagnostics(config.errors)); + } + + this._rootNames = config.rootNames.concat(...this._singleFileIncludes); + this._compilerOptions = { ...config.options, ...options.compilerOptions }; + this._basePath = config.options.basePath || ''; + + // Overwrite outDir so we can find generated files next to their .ts origin in compilerHost. + this._compilerOptions.outDir = ''; + this._compilerOptions.suppressOutputPathCheck = true; + + // Default plugin sourceMap to compiler options setting. + if (!options.hasOwnProperty('sourceMap')) { + options.sourceMap = this._compilerOptions.sourceMap || false; + } + + // Force the right sourcemap options. + if (options.sourceMap) { + this._compilerOptions.sourceMap = true; + this._compilerOptions.inlineSources = true; + this._compilerOptions.inlineSourceMap = false; + this._compilerOptions.mapRoot = undefined; + // We will set the source to the full path of the file in the loader, so we don't + // need sourceRoot here. + this._compilerOptions.sourceRoot = undefined; + } else { + this._compilerOptions.sourceMap = false; + this._compilerOptions.sourceRoot = undefined; + this._compilerOptions.inlineSources = undefined; + this._compilerOptions.inlineSourceMap = undefined; + this._compilerOptions.mapRoot = undefined; + this._compilerOptions.sourceRoot = undefined; + } + + // We want to allow emitting with errors so that imports can be added + // to the webpack dependency tree and rebuilds triggered by file edits. + this._compilerOptions.noEmitOnError = false; + + // Set JIT (no code generation) or AOT mode. + if (options.skipCodeGeneration !== undefined) { + this._JitMode = options.skipCodeGeneration; + } + + // Process i18n options. + if (options.i18nInFile !== undefined) { + this._compilerOptions.i18nInFile = options.i18nInFile; + } + if (options.i18nInFormat !== undefined) { + this._compilerOptions.i18nInFormat = options.i18nInFormat; + } + if (options.i18nOutFile !== undefined) { + this._compilerOptions.i18nOutFile = options.i18nOutFile; + } + if (options.i18nOutFormat !== undefined) { + this._compilerOptions.i18nOutFormat = options.i18nOutFormat; + } + if (options.locale !== undefined) { + this._compilerOptions.i18nInLocale = options.locale; + this._compilerOptions.i18nOutLocale = options.locale; + this._normalizedLocale = this._validateLocale(options.locale); + } + if (options.missingTranslation !== undefined) { + this._compilerOptions.i18nInMissingTranslations = + options.missingTranslation as 'error' | 'warning' | 'ignore'; + } + + // Process forked type checker options. + if (options.forkTypeChecker !== undefined) { + this._forkTypeChecker = options.forkTypeChecker; + } + + // Create the webpack compiler host. + const webpackCompilerHost = new WebpackCompilerHost( + this._compilerOptions, + this._basePath, + this._options.host, + ); + webpackCompilerHost.enableCaching(); + + // Create and set a new WebpackResourceLoader. + this._resourceLoader = new WebpackResourceLoader(); + webpackCompilerHost.setResourceLoader(this._resourceLoader); + + // Use the WebpackCompilerHost with a resource loader to create an AngularCompilerHost. + this._compilerHost = createCompilerHost({ + options: this._compilerOptions, + tsHost: webpackCompilerHost, + }) as CompilerHost & WebpackCompilerHost; + + // Override some files in the FileSystem with paths from the actual file system. + if (this._options.hostReplacementPaths) { + for (const filePath of Object.keys(this._options.hostReplacementPaths)) { + const replacementFilePath = this._options.hostReplacementPaths[filePath]; + const content = this._compilerHost.readFile(replacementFilePath); + if (content) { + this._compilerHost.writeFile(filePath, content, false); + } + } + } + + // Resolve mainPath if provided. + if (options.mainPath) { + this._mainPath = this._compilerHost.resolve(options.mainPath); + } + + // Use entryModule if available in options, otherwise resolve it from mainPath after program + // creation. + if (this._options.entryModule) { + this._entryModule = this._options.entryModule; + } else if (this._compilerOptions.entryModule) { + this._entryModule = path.resolve(this._basePath, + this._compilerOptions.entryModule as string); // temporary cast for type issue + } + + // Set platform. + this._platform = options.platform || PLATFORM.Browser; + + // Make transformers. + this._makeTransformers(); + + timeEnd('AngularCompilerPlugin._setupOptions'); + } + + private _getTsProgram() { + return this._JitMode ? this._program as ts.Program : (this._program as Program).getTsProgram(); + } + + private _getChangedTsFiles() { + return this._compilerHost.getChangedFilePaths() + .filter(k => k.endsWith('.ts') && !k.endsWith('.d.ts')) + .filter(k => this._compilerHost.fileExists(k)); + } + + updateChangedFileExtensions(extension: string) { + if (extension) { + this._changedFileExtensions.add(extension); + } + } + + private _getChangedCompilationFiles() { + return this._compilerHost.getChangedFilePaths() + .filter(k => { + for (const ext of this._changedFileExtensions) { + if (k.endsWith(ext)) { + return true; + } + } + + return false; + }); + } + + private _createOrUpdateProgram() { + return Promise.resolve() + .then(() => { + // Get the root files from the ts config. + // When a new root name (like a lazy route) is added, it won't be available from + // following imports on the existing files, so we need to get the new list of root files. + const config = readConfiguration(this._tsConfigPath); + this._rootNames = config.rootNames.concat(...this._singleFileIncludes); + + // Update the forked type checker with all changed compilation files. + // This includes templates, that also need to be reloaded on the type checker. + if (this._forkTypeChecker && this._typeCheckerProcess && !this._firstRun) { + this._updateForkedTypeChecker(this._rootNames, this._getChangedCompilationFiles()); + } + + // Use an identity function as all our paths are absolute already. + this._moduleResolutionCache = ts.createModuleResolutionCache(this._basePath, x => x); + + if (this._JitMode) { + // Create the TypeScript program. + time('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram'); + this._program = ts.createProgram( + this._rootNames, + this._compilerOptions, + this._compilerHost, + this._program as ts.Program, + ); + timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram'); + + return Promise.resolve(); + } else { + time('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram'); + // Create the Angular program. + this._program = createProgram({ + rootNames: this._rootNames, + options: this._compilerOptions, + host: this._compilerHost, + oldProgram: this._program as Program, + }); + timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram'); + + time('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync'); + + return this._program.loadNgStructureAsync() + .then(() => { + timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync'); + }); + } + }) + .then(() => { + // If there's still no entryModule try to resolve from mainPath. + if (!this._entryModule && this._mainPath) { + time('AngularCompilerPlugin._make.resolveEntryModuleFromMain'); + this._entryModule = resolveEntryModuleFromMain( + this._mainPath, this._compilerHost, this._getTsProgram()); + timeEnd('AngularCompilerPlugin._make.resolveEntryModuleFromMain'); + } + }); + } + + private _getLazyRoutesFromNgtools() { + try { + time('AngularCompilerPlugin._getLazyRoutesFromNgtools'); + const result = __NGTOOLS_PRIVATE_API_2.listLazyRoutes({ + program: this._getTsProgram(), + host: this._compilerHost, + angularCompilerOptions: Object.assign({}, this._compilerOptions, { + // genDir seems to still be needed in @angular\compiler-cli\src\compiler_host.js:226. + genDir: '', + }), + // TODO: fix compiler-cli typings; entryModule should not be string, but also optional. + // tslint:disable-next-line:non-null-operator + entryModule: this._entryModule !, + }); + timeEnd('AngularCompilerPlugin._getLazyRoutesFromNgtools'); + + return result; + } catch (err) { + // We silence the error that the @angular/router could not be found. In that case, there is + // basically no route supported by the app itself. + if (err.message.startsWith('Could not resolve module @angular/router')) { + return {}; + } else { + throw err; + } + } + } + + private _findLazyRoutesInAst(changedFilePaths: string[]): LazyRouteMap { + time('AngularCompilerPlugin._findLazyRoutesInAst'); + const result: LazyRouteMap = Object.create(null); + for (const filePath of changedFilePaths) { + const fileLazyRoutes = findLazyRoutes(filePath, this._compilerHost, undefined, + this._compilerOptions); + for (const routeKey of Object.keys(fileLazyRoutes)) { + const route = fileLazyRoutes[routeKey]; + result[routeKey] = route; + } + } + timeEnd('AngularCompilerPlugin._findLazyRoutesInAst'); + + return result; + } + + private _listLazyRoutesFromProgram(): LazyRouteMap { + const ngProgram = this._program as Program; + if (!ngProgram.listLazyRoutes) { + throw new Error('_listLazyRoutesFromProgram was called with an old program.'); + } + + const lazyRoutes = ngProgram.listLazyRoutes(); + + return lazyRoutes.reduce( + (acc, curr) => { + const ref = curr.route; + if (ref in acc && acc[ref] !== curr.referencedModule.filePath) { + throw new Error( + + `Duplicated path in loadChildren detected: "${ref}" is used in 2 loadChildren, ` + + `but they point to different modules "(${acc[ref]} and ` + + `"${curr.referencedModule.filePath}"). Webpack cannot distinguish on context and ` + + 'would fail to load the proper one.', + ); + } + acc[ref] = curr.referencedModule.filePath; + + return acc; + }, + {} as LazyRouteMap, + ); + } + + // Process the lazy routes discovered, adding then to _lazyRoutes. + // TODO: find a way to remove lazy routes that don't exist anymore. + // This will require a registry of known references to a lazy route, removing it when no + // module references it anymore. + private _processLazyRoutes(discoveredLazyRoutes: LazyRouteMap) { + Object.keys(discoveredLazyRoutes) + .forEach(lazyRouteKey => { + const [lazyRouteModule, moduleName] = lazyRouteKey.split('#'); + + if (!lazyRouteModule) { + return; + } + + const lazyRouteTSFile = discoveredLazyRoutes[lazyRouteKey].replace(/\\/g, '/'); + let modulePath: string, moduleKey: string; + + if (this._JitMode) { + modulePath = lazyRouteTSFile; + moduleKey = `${lazyRouteModule}${moduleName ? '#' + moduleName : ''}`; + } else { + modulePath = lazyRouteTSFile.replace(/(\.d)?\.ts$/, `.ngfactory.js`); + const factoryModuleName = moduleName ? `#${moduleName}NgFactory` : ''; + moduleKey = `${lazyRouteModule}.ngfactory${factoryModuleName}`; + } + + modulePath = workaroundResolve(modulePath); + + if (moduleKey in this._lazyRoutes) { + if (this._lazyRoutes[moduleKey] !== modulePath) { + // Found a duplicate, this is an error. + this._warnings.push( + new Error(`Duplicated path in loadChildren detected during a rebuild. ` + + `We will take the latest version detected and override it to save rebuild time. ` + + `You should perform a full build to validate that your routes don't overlap.`), + ); + } + } else { + // Found a new route, add it to the map. + this._lazyRoutes[moduleKey] = modulePath; + } + }); + } + + private _createForkedTypeChecker() { + // Bootstrap type checker is using local CLI. + const g: any = typeof global !== 'undefined' ? global : {}; // tslint:disable-line:no-any + const typeCheckerFile: string = g['_DevKitIsLocal'] + ? './type_checker_bootstrap.js' + : './type_checker_worker.js'; + + const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/; + + const execArgv = process.execArgv.filter((arg) => { + // Remove debug args. + // Workaround for https://github.com/nodejs/node/issues/9435 + return !debugArgRegex.test(arg); + }); + // Signal the process to start listening for messages + // Solves https://github.com/angular/angular-cli/issues/9071 + const forkArgs = [AUTO_START_ARG]; + const forkOptions: ForkOptions = { execArgv }; + + this._typeCheckerProcess = fork( + path.resolve(__dirname, typeCheckerFile), + forkArgs, + forkOptions); + + // Handle child process exit. + this._typeCheckerProcess.once('exit', (_, signal) => { + this._typeCheckerProcess = null; + + // If process exited not because of SIGTERM (see _killForkedTypeChecker), than something + // went wrong and it should fallback to type checking on the main thread. + if (signal !== 'SIGTERM') { + this._forkTypeChecker = false; + const msg = 'AngularCompilerPlugin: Forked Type Checker exited unexpectedly. ' + + 'Falling back to type checking on main thread.'; + this._warnings.push(msg); + } + }); + } + + private _killForkedTypeChecker() { + if (this._typeCheckerProcess && this._typeCheckerProcess.pid) { + treeKill(this._typeCheckerProcess.pid, 'SIGTERM'); + this._typeCheckerProcess = null; + } + } + + private _updateForkedTypeChecker(rootNames: string[], changedCompilationFiles: string[]) { + if (this._typeCheckerProcess) { + if (!this._forkedTypeCheckerInitialized) { + this._typeCheckerProcess.send(new InitMessage(this._compilerOptions, this._basePath, + this._JitMode, this._rootNames)); + this._forkedTypeCheckerInitialized = true; + } + this._typeCheckerProcess.send(new UpdateMessage(rootNames, changedCompilationFiles)); + } + } + + // Registration hook for webpack plugin. + // tslint:disable-next-line:no-any + apply(compiler: any) { + // Decorate inputFileSystem to serve contents of CompilerHost. + // Use decorated inputFileSystem in watchFileSystem. + compiler.hooks.environment.tap('angular-compiler', () => { + compiler.inputFileSystem = new VirtualFileSystemDecorator( + compiler.inputFileSystem, this._compilerHost); + compiler.watchFileSystem = new VirtualWatchFileSystemDecorator(compiler.inputFileSystem); + }); + + // Add lazy modules to the context module for @angular/core + compiler.hooks.contextModuleFactory.tap('angular-compiler', (cmf: any) => { + const angularCorePackagePath = require.resolve('@angular/core/package.json'); + + // APFv6 does not have single FESM anymore. Instead of verifying if we're pointing to + // FESMs, we resolve the `@angular/core` path and verify that the path for the + // module starts with it. + + // This may be slower but it will be compatible with both APF5, 6 and potential future + // versions (until the dynamic import appears outside of core I suppose). + // We resolve any symbolic links in order to get the real path that would be used in webpack. + const angularCoreDirname = fs.realpathSync(path.dirname(angularCorePackagePath)); + + cmf.hooks.afterResolve.tapAsync('angular-compiler', + // tslint:disable-next-line:no-any + (result: any, callback: (err?: Error, request?: any) => void) => { + if (!result) { + return callback(); + } + + // Alter only request from Angular. + if (!result.resource.startsWith(angularCoreDirname)) { + return callback(undefined, result); + } + if (!this.done) { + return callback(undefined, result); + } + + this.done.then(() => { + // This folder does not exist, but we need to give webpack a resource. + // TODO: check if we can't just leave it as is (angularCoreModuleDir). + result.resource = path.join(this._basePath, '$$_lazy_route_resource'); + result.dependencies.forEach((d: any) => d.critical = false); + result.resolveDependencies = (_fs: any, resourceOrOptions: any, recursiveOrCallback: any, + _regExp: RegExp, cb: any) => { + const dependencies = Object.keys(this._lazyRoutes) + .map((key) => { + const modulePath = this._lazyRoutes[key]; + const importPath = key.split('#')[0]; + if (modulePath !== null) { + const name = importPath.replace(/(\.ngfactory)?\.(js|ts)$/, ''); + + return new ContextElementDependency(modulePath, name); + } else { + return null; + } + }) + .filter(x => !!x); + if (typeof cb !== 'function' && typeof recursiveOrCallback === 'function') { + // Webpack 4 only has 3 parameters + cb = recursiveOrCallback; + if (this._options.nameLazyFiles) { + resourceOrOptions.chunkName = '[request]'; + } + } + cb(null, dependencies); + }; + + return callback(undefined, result); + }, () => callback()) + .catch(err => callback(err)); + }); + }); + + // Create and destroy forked type checker on watch mode. + compiler.hooks.watchRun.tapAsync('angular-compiler', (_compiler: any, callback: any) => { + if (this._forkTypeChecker && !this._typeCheckerProcess) { + this._createForkedTypeChecker(); + } + callback(); + }); + compiler.hooks.watchClose.tap('angular-compiler', () => this._killForkedTypeChecker()); + + // Remake the plugin on each compilation. + compiler.hooks.make.tapAsync( + 'angular-compiler', + (compilation: any, cb: any) => this._make(compilation, cb), + ); + compiler.hooks.invalid.tap('angular-compiler', () => this._firstRun = false); + compiler.hooks.afterEmit.tapAsync('angular-compiler', (compilation: any, cb: any) => { + compilation._ngToolsWebpackPluginInstance = null; + cb(); + }); + compiler.hooks.done.tap('angular-compiler', () => { + this._donePromise = null; + }); + + compiler.hooks.afterResolvers.tap('angular-compiler', (compiler: any) => { + compiler.hooks.normalModuleFactory.tap('angular-compiler', (nmf: any) => { + // Virtual file system. + // TODO: consider if it's better to remove this plugin and instead make it wait on the + // VirtualFileSystemDecorator. + // Wait for the plugin to be done when requesting `.ts` files directly (entry points), or + // when the issuer is a `.ts` or `.ngfactory.js` file. + nmf.hooks.beforeResolve.tapAsync('angular-compiler', (request: any, callback: any) => { + if (this.done && (request.request.endsWith('.ts') + || (request.context.issuer && /\.ts|ngfactory\.js$/.test(request.context.issuer)))) { + this.done.then(() => callback(null, request), () => callback(null, request)); + } else { + callback(null, request); + } + }); + }); + }); + + compiler.hooks.normalModuleFactory.tap('angular-compiler', (nmf: any) => { + nmf.hooks.beforeResolve.tapAsync('angular-compiler', (request: any, callback: any) => { + resolveWithPaths( + request, + callback, + this._compilerOptions, + this._compilerHost, + this._moduleResolutionCache, + ); + }); + }); + } + + private _make(compilation: any, cb: (err?: any, request?: any) => void) { + time('AngularCompilerPlugin._make'); + this._emitSkipped = true; + if (compilation._ngToolsWebpackPluginInstance) { + return cb(new Error('An @ngtools/webpack plugin already exist for this compilation.')); + } + + // Set a private variable for this plugin instance. + compilation._ngToolsWebpackPluginInstance = this; + + // Update the resource loader with the new webpack compilation. + this._resourceLoader.update(compilation); + + this._donePromise = Promise.resolve() + .then(() => this._update()) + .then(() => { + this.pushCompilationErrors(compilation); + timeEnd('AngularCompilerPlugin._make'); + cb(); + }, (err: any) => { + compilation.errors.push(err); + this.pushCompilationErrors(compilation); + timeEnd('AngularCompilerPlugin._make'); + cb(); + }); + } + + private pushCompilationErrors(compilation: any) { + compilation.errors.push(...this._errors); + compilation.warnings.push(...this._warnings); + this._errors = []; + this._warnings = []; + } + + private _makeTransformers() { + const isAppPath = (fileName: string) => + !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts'); + const isMainPath = (fileName: string) => fileName === ( + this._mainPath ? workaroundResolve(this._mainPath) : this._mainPath + ); + const getEntryModule = () => this.entryModule + ? { path: workaroundResolve(this.entryModule.path), className: this.entryModule.className } + : this.entryModule; + const getLazyRoutes = () => this._lazyRoutes; + const getTypeChecker = () => this._getTsProgram().getTypeChecker(); + + if (this._JitMode) { + // Replace resources in JIT. + this._transformers.push(replaceResources(isAppPath)); + } else { + // Remove unneeded angular decorators. + this._transformers.push(removeDecorators(isAppPath, getTypeChecker)); + } + + if (this._platform === PLATFORM.Browser) { + // If we have a locale, auto import the locale data file. + // This transform must go before replaceBootstrap because it looks for the entry module + // import, which will be replaced. + if (this._normalizedLocale) { + this._transformers.push(registerLocaleData(isAppPath, getEntryModule, + this._normalizedLocale)); + } + + if (!this._JitMode) { + // Replace bootstrap in browser AOT. + this._transformers.push(replaceBootstrap(isAppPath, getEntryModule, getTypeChecker)); + } + } else if (this._platform === PLATFORM.Server) { + this._transformers.push(exportLazyModuleMap(isMainPath, getLazyRoutes)); + if (!this._JitMode) { + this._transformers.push( + exportNgFactory(isMainPath, getEntryModule), + replaceServerBootstrap(isMainPath, getEntryModule, getTypeChecker)); + } + } + } + + private _update() { + time('AngularCompilerPlugin._update'); + // We only want to update on TS and template changes, but all kinds of files are on this + // list, like package.json and .ngsummary.json files. + const changedFiles = this._getChangedCompilationFiles(); + + // If nothing we care about changed and it isn't the first run, don't do anything. + if (changedFiles.length === 0 && !this._firstRun) { + return Promise.resolve(); + } + + return Promise.resolve() + // Make a new program and load the Angular structure. + .then(() => this._createOrUpdateProgram()) + .then(() => { + if (this.entryModule) { + // Try to find lazy routes if we have an entry module. + // We need to run the `listLazyRoutes` the first time because it also navigates libraries + // and other things that we might miss using the (faster) findLazyRoutesInAst. + // Lazy routes modules will be read with compilerHost and added to the changed files. + const changedTsFiles = this._getChangedTsFiles(); + if (this._ngCompilerSupportsNewApi) { + this._processLazyRoutes(this._listLazyRoutesFromProgram()); + } else if (this._firstRun) { + this._processLazyRoutes(this._getLazyRoutesFromNgtools()); + } else if (changedTsFiles.length > 0) { + this._processLazyRoutes(this._findLazyRoutesInAst(changedTsFiles)); + } + if (this._options.additionalLazyModules) { + this._processLazyRoutes(this._options.additionalLazyModules); + } + } + }) + .then(() => { + // Emit and report errors. + + // We now have the final list of changed TS files. + // Go through each changed file and add transforms as needed. + const sourceFiles = this._getChangedTsFiles() + .map((fileName) => this._getTsProgram().getSourceFile(fileName)) + // At this point we shouldn't need to filter out undefined files, because any ts file + // that changed should be emitted. + // But due to hostReplacementPaths there can be files (the environment files) + // that changed but aren't part of the compilation, specially on `ng test`. + // So we ignore missing source files files here. + // hostReplacementPaths needs to be fixed anyway to take care of the following issue. + // https://github.com/angular/angular-cli/issues/7305#issuecomment-332150230 + .filter((x) => !!x) as ts.SourceFile[]; + + // Emit files. + time('AngularCompilerPlugin._update._emit'); + const { emitResult, diagnostics } = this._emit(sourceFiles); + timeEnd('AngularCompilerPlugin._update._emit'); + + // Report diagnostics. + const errors = diagnostics + .filter((diag) => diag.category === ts.DiagnosticCategory.Error); + const warnings = diagnostics + .filter((diag) => diag.category === ts.DiagnosticCategory.Warning); + + if (errors.length > 0) { + const message = formatDiagnostics(errors); + this._errors.push(new Error(message)); + } + + if (warnings.length > 0) { + const message = formatDiagnostics(warnings); + this._warnings.push(message); + } + + this._emitSkipped = !emitResult || emitResult.emitSkipped; + + // Reset changed files on successful compilation. + if (!this._emitSkipped && this._errors.length === 0) { + this._compilerHost.resetChangedFileTracker(); + } + timeEnd('AngularCompilerPlugin._update'); + }); + } + + writeI18nOutFile() { + function _recursiveMkDir(p: string): Promise { + if (fs.existsSync(p)) { + return Promise.resolve(); + } else { + return _recursiveMkDir(path.dirname(p)) + .then(() => fs.mkdirSync(p)); + } + } + + // Write the extracted messages to disk. + if (this._compilerOptions.i18nOutFile) { + const i18nOutFilePath = path.resolve(this._basePath, this._compilerOptions.i18nOutFile); + const i18nOutFileContent = this._compilerHost.readFile(i18nOutFilePath); + if (i18nOutFileContent) { + _recursiveMkDir(path.dirname(i18nOutFilePath)) + .then(() => fs.writeFileSync(i18nOutFilePath, i18nOutFileContent)); + } + } + } + + getCompiledFile(fileName: string) { + const outputFile = fileName.replace(/.ts$/, '.js'); + let outputText: string; + let sourceMap: string | undefined; + let errorDependencies: string[] = []; + + if (this._emitSkipped) { + const text = this._compilerHost.readFile(outputFile); + if (text) { + // If the compilation didn't emit files this time, try to return the cached files from the + // last compilation and let the compilation errors show what's wrong. + outputText = text; + sourceMap = this._compilerHost.readFile(outputFile + '.map'); + } else { + // There's nothing we can serve. Return an empty string to prevent lenghty webpack errors, + // add the rebuild warning if it's not there yet. + // We also need to all changed files as dependencies of this file, so that all of them + // will be watched and trigger a rebuild next time. + outputText = ''; + errorDependencies = this._getChangedCompilationFiles() + // These paths are used by the loader so we must denormalize them. + .map((p) => this._compilerHost.denormalizePath(p)); + } + } else { + // Check if the TS input file and the JS output file exist. + if ((fileName.endsWith('.ts') && !this._compilerHost.fileExists(fileName, false)) + || !this._compilerHost.fileExists(outputFile, false)) { + let msg = `${fileName} is missing from the TypeScript compilation. ` + + `Please make sure it is in your tsconfig via the 'files' or 'include' property.`; + + if (/(\\|\/)node_modules(\\|\/)/.test(fileName)) { + msg += '\nThe missing file seems to be part of a third party library. ' + + 'TS files in published libraries are often a sign of a badly packaged library. ' + + 'Please open an issue in the library repository to alert its author and ask them ' + + 'to package the library using the Angular Package Format (https://goo.gl/jB3GVv).'; + } + + throw new Error(msg); + } + + outputText = this._compilerHost.readFile(outputFile) || ''; + sourceMap = this._compilerHost.readFile(outputFile + '.map'); + } + + return { outputText, sourceMap, errorDependencies }; + } + + getDependencies(fileName: string): string[] { + const resolvedFileName = this._compilerHost.resolve(fileName); + const sourceFile = this._compilerHost.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest); + if (!sourceFile) { + return []; + } + + const options = this._compilerOptions; + const host = this._compilerHost; + const cache = this._moduleResolutionCache; + + const esImports = collectDeepNodes(sourceFile, + ts.SyntaxKind.ImportDeclaration) + .map(decl => { + const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text; + const resolved = ts.resolveModuleName(moduleName, resolvedFileName, options, host, cache); + + if (resolved.resolvedModule) { + return resolved.resolvedModule.resolvedFileName; + } else { + return null; + } + }) + .filter(x => x); + + const resourceImports = findResources(sourceFile) + .map((resourceReplacement) => resourceReplacement.resourcePaths) + .reduce((prev, curr) => prev.concat(curr), []) + .map((resourcePath) => resolve(dirname(resolvedFileName), normalize(resourcePath))); + + // These paths are meant to be used by the loader so we must denormalize them. + const uniqueDependencies = new Set([ + ...esImports, + ...resourceImports, + ...this.getResourceDependencies(this._compilerHost.denormalizePath(resolvedFileName)), + ].map((p) => p && this._compilerHost.denormalizePath(p))); + + return [...uniqueDependencies] + .filter(x => !!x) as string[]; + } + + getResourceDependencies(fileName: string): string[] { + return this._resourceLoader.getResourceDependencies(fileName); + } + + // This code mostly comes from `performCompilation` in `@angular/compiler-cli`. + // It skips the program creation because we need to use `loadNgStructureAsync()`, + // and uses CustomTransformers. + private _emit(sourceFiles: ts.SourceFile[]) { + time('AngularCompilerPlugin._emit'); + const program = this._program; + const allDiagnostics: Array = []; + + let emitResult: ts.EmitResult | undefined; + try { + if (this._JitMode) { + const tsProgram = program as ts.Program; + + if (this._firstRun) { + // Check parameter diagnostics. + time('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics'); + allDiagnostics.push(...tsProgram.getOptionsDiagnostics()); + timeEnd('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics'); + } + + if ((this._firstRun || !this._forkTypeChecker) && this._program) { + allDiagnostics.push(...gatherDiagnostics(this._program, this._JitMode, + 'AngularCompilerPlugin._emit.ts')); + } + + if (!hasErrors(allDiagnostics)) { + sourceFiles.forEach((sf) => { + const timeLabel = `AngularCompilerPlugin._emit.ts+${sf.fileName}+.emit`; + time(timeLabel); + emitResult = tsProgram.emit(sf, undefined, undefined, undefined, + { before: this._transformers }, + ); + allDiagnostics.push(...emitResult.diagnostics); + timeEnd(timeLabel); + }); + } + } else { + const angularProgram = program as Program; + + // Check Angular structural diagnostics. + time('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics'); + allDiagnostics.push(...angularProgram.getNgStructuralDiagnostics()); + timeEnd('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics'); + + if (this._firstRun) { + // Check TypeScript parameter diagnostics. + time('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics'); + allDiagnostics.push(...angularProgram.getTsOptionDiagnostics()); + timeEnd('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics'); + + // Check Angular parameter diagnostics. + time('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics'); + allDiagnostics.push(...angularProgram.getNgOptionDiagnostics()); + timeEnd('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics'); + } + + if ((this._firstRun || !this._forkTypeChecker) && this._program) { + allDiagnostics.push(...gatherDiagnostics(this._program, this._JitMode, + 'AngularCompilerPlugin._emit.ng')); + } + + if (!hasErrors(allDiagnostics)) { + time('AngularCompilerPlugin._emit.ng.emit'); + const extractI18n = !!this._compilerOptions.i18nOutFile; + const emitFlags = extractI18n ? EmitFlags.I18nBundle : EmitFlags.Default; + emitResult = angularProgram.emit({ + emitFlags, customTransformers: { + beforeTs: this._transformers, + }, + }); + allDiagnostics.push(...emitResult.diagnostics); + if (extractI18n) { + this.writeI18nOutFile(); + } + timeEnd('AngularCompilerPlugin._emit.ng.emit'); + } + } + } catch (e) { + time('AngularCompilerPlugin._emit.catch'); + // This function is available in the import below, but this way we avoid the dependency. + // import { isSyntaxError } from '@angular/compiler'; + function isSyntaxError(error: Error): boolean { + return (error as any)['ngSyntaxError']; // tslint:disable-line:no-any + } + + let errMsg: string; + let code: number; + if (isSyntaxError(e)) { + // don't report the stack for syntax errors as they are well known errors. + errMsg = e.message; + code = DEFAULT_ERROR_CODE; + } else { + errMsg = e.stack; + // It is not a syntax error we might have a program with unknown state, discard it. + this._program = null; + code = UNKNOWN_ERROR_CODE; + } + allDiagnostics.push( + { category: ts.DiagnosticCategory.Error, messageText: errMsg, code, source: SOURCE }); + timeEnd('AngularCompilerPlugin._emit.catch'); + } + timeEnd('AngularCompilerPlugin._emit'); + + return { program, emitResult, diagnostics: allDiagnostics }; + } + + private _validateLocale(locale: string): string | null { + // Get the path of the common module. + const commonPath = path.dirname(require.resolve('@angular/common/package.json')); + // Check if the locale file exists + if (!fs.existsSync(path.resolve(commonPath, 'locales', `${locale}.js`))) { + // Check for an alternative locale (if the locale id was badly formatted). + const locales = fs.readdirSync(path.resolve(commonPath, 'locales')) + .filter(file => file.endsWith('.js')) + .map(file => file.replace('.js', '')); + + let newLocale; + const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); + for (const l of locales) { + if (l.toLowerCase() === normalizedLocale) { + newLocale = l; + break; + } + } + + if (newLocale) { + locale = newLocale; + } else { + // Check for a parent locale + const parentLocale = normalizedLocale.split('-')[0]; + if (locales.indexOf(parentLocale) !== -1) { + locale = parentLocale; + } else { + this._warnings.push(`AngularCompilerPlugin: Unable to load the locale data file ` + + `"@angular/common/locales/${locale}", ` + + `please check that "${locale}" is a valid locale id. + If needed, you can use "registerLocaleData" manually.`); + + return null; + } + } + } + + return locale; + } +} diff --git a/packages/ngtools/webpack/src/benchmark.ts b/packages/ngtools/webpack/src/benchmark.ts new file mode 100644 index 0000000000..978dfe0126 --- /dev/null +++ b/packages/ngtools/webpack/src/benchmark.ts @@ -0,0 +1,23 @@ +/** + * @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 + */ +// Internal benchmark reporting flag. +// Use with CLI --no-progress flag for best results. +// This should be false for commited code. +const _benchmark = false; + +export function time(label: string) { + if (_benchmark) { + console.time(label); + } +} + +export function timeEnd(label: string) { + if (_benchmark) { + console.timeEnd(label); + } +} diff --git a/packages/ngtools/webpack/src/compiler_host.ts b/packages/ngtools/webpack/src/compiler_host.ts new file mode 100644 index 0000000000..7ed4d444fe --- /dev/null +++ b/packages/ngtools/webpack/src/compiler_host.ts @@ -0,0 +1,391 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, getSystemPath, join as _join, normalize, virtualFs } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import * as fs from 'fs'; +import { basename, dirname, join } from 'path'; +import * as ts from 'typescript'; +import { WebpackResourceLoader } from './resource_loader'; + + +export interface OnErrorFn { + (message: string): void; +} + + +const dev = Math.floor(Math.random() * 10000); + + +export class VirtualStats implements fs.Stats { + protected _ctime = new Date(); + protected _mtime = new Date(); + protected _atime = new Date(); + protected _btime = new Date(); + protected _dev = dev; + protected _ino = Math.floor(Math.random() * 100000); + protected _mode = parseInt('777', 8); // RWX for everyone. + protected _uid = Number(process.env['UID']) || 0; + protected _gid = Number(process.env['GID']) || 0; + + constructor(protected _path: string) {} + + isFile() { return false; } + isDirectory() { return false; } + isBlockDevice() { return false; } + isCharacterDevice() { return false; } + isSymbolicLink() { return false; } + isFIFO() { return false; } + isSocket() { return false; } + + get dev() { return this._dev; } + get ino() { return this._ino; } + get mode() { return this._mode; } + get nlink() { return 1; } // Default to 1 hard link. + get uid() { return this._uid; } + get gid() { return this._gid; } + get rdev() { return 0; } + get size() { return 0; } + get blksize() { return 512; } + get blocks() { return Math.ceil(this.size / this.blksize); } + get atime() { return this._atime; } + get atimeMs() { return this._atime.getTime(); } + get mtime() { return this._mtime; } + get mtimeMs() { return this._mtime.getTime(); } + get ctime() { return this._ctime; } + get ctimeMs() { return this._ctime.getTime(); } + get birthtime() { return this._btime; } + get birthtimeMs() { return this._btime.getTime(); } +} + +export class VirtualDirStats extends VirtualStats { + constructor(_fileName: string) { + super(_fileName); + } + + isDirectory() { return true; } + + get size() { return 1024; } +} + +export class VirtualFileStats extends VirtualStats { + private _sourceFile: ts.SourceFile | null; + constructor(_fileName: string, private _content: string) { + super(_fileName); + } + + get content() { return this._content; } + set content(v: string) { + this._content = v; + this._mtime = new Date(); + this._sourceFile = null; + } + setSourceFile(sourceFile: ts.SourceFile) { + this._sourceFile = sourceFile; + } + getSourceFile(languageVersion: ts.ScriptTarget, setParentNodes: boolean) { + if (!this._sourceFile) { + // console.log(this._path) + this._sourceFile = ts.createSourceFile( + workaroundResolve(this._path), + this._content, + languageVersion, + setParentNodes); + } + + return this._sourceFile; + } + + isFile() { return true; } + + get size() { return this._content.length; } +} + + +export class WebpackCompilerHost implements ts.CompilerHost { + private _syncHost: virtualFs.SyncDelegateHost; + private _files: {[path: string]: VirtualFileStats | null} = Object.create(null); + private _directories: {[path: string]: VirtualDirStats | null} = Object.create(null); + + private _changedFiles: {[path: string]: boolean} = Object.create(null); + private _changedDirs: {[path: string]: boolean} = Object.create(null); + + private _basePath: string; + private _setParentNodes: boolean; + + private _cache = false; + private _resourceLoader?: WebpackResourceLoader | undefined; + + constructor( + private _options: ts.CompilerOptions, + basePath: string, + private _host: virtualFs.Host = new NodeJsSyncHost(), + ) { + this._syncHost = new virtualFs.SyncDelegateHost(_host); + this._setParentNodes = true; + this._basePath = this._normalizePath(basePath); + } + + private _normalizePath(path: string): Path { + return normalize(path); + } + + denormalizePath(path: string) { + return getSystemPath(normalize(path)); + } + + resolve(path: string): Path { + const p = this._normalizePath(path); + if (p[0] == '.') { + return this._normalizePath(join(this.getCurrentDirectory(), p)); + } else if (p[0] == '/' || p.match(/^\w:\//)) { + return p; + } else { + return this._normalizePath(join(this._basePath, p)); + } + } + + private _setFileContent(fileName: Path, content: string) { + this._files[fileName] = new VirtualFileStats(fileName, content); + + let p = dirname(fileName); + while (p && !this._directories[p]) { + this._directories[p] = new VirtualDirStats(p); + this._changedDirs[p] = true; + p = dirname(p); + } + + this._changedFiles[fileName] = true; + } + + get dirty() { + return Object.keys(this._changedFiles).length > 0; + } + + enableCaching() { + this._cache = true; + } + + resetChangedFileTracker() { + this._changedFiles = Object.create(null); + this._changedDirs = Object.create(null); + } + + getChangedFilePaths(): string[] { + return Object.keys(this._changedFiles); + } + + getNgFactoryPaths(): string[] { + return Object.keys(this._files) + .filter(fileName => fileName.endsWith('.ngfactory.js') || fileName.endsWith('.ngstyle.js')) + // These paths are used by the virtual file system decorator so we must denormalize them. + .map(path => this.denormalizePath(path as Path)); + } + + invalidate(fileName: string): void { + const fullPath = this.resolve(fileName); + + if (fullPath in this._files) { + this._files[fullPath] = null; + } else { + for (const file in this._files) { + if (file.startsWith(fullPath + '/')) { + this._files[file] = null; + } + } + } + if (this.fileExists(fullPath)) { + this._changedFiles[fullPath] = true; + } + } + + fileExists(fileName: string, delegate = true): boolean { + const p = this.resolve(fileName); + + return this._files[p] != null + || (delegate && this._syncHost.exists(normalize(p))); + } + + readFile(fileName: string): string | undefined { + const p = this.resolve(fileName); + + const stats = this._files[p]; + if (!stats) { + try { + const result = virtualFs.fileBufferToString(this._syncHost.read(p)); + if (result !== undefined) { + if (this._cache) { + this._setFileContent(p, result); + } + } + + return result; + } catch (e) { + return undefined; + } + } + + return stats.content; + } + + // Does not delegate, use with `fileExists/directoryExists()`. + stat(path: string): VirtualStats { + const p = this.resolve(path); + const stats = this._files[p] || this._directories[p]; + if (!stats) { + throw new Error(`File not found: ${JSON.stringify(p)}`); + } + + return stats; + } + + directoryExists(directoryName: string, delegate = true): boolean { + const p = this.resolve(directoryName); + + return (this._directories[p] != null) + || (delegate && this._syncHost.exists(p) && this._syncHost.isDirectory(p)); + } + + getFiles(path: string): string[] { + const p = this.resolve(path); + + const subfiles = Object.keys(this._files) + .filter(fileName => dirname(fileName) == p) + .map(p => basename(p)); + + + let delegated: string[]; + try { + delegated = this._syncHost.list(p).filter((x: string) => { + try { + return this._syncHost.isFile(_join(p, x)); + } catch (e) { + return false; + } + }); + } catch (e) { + delegated = []; + } + + return delegated.concat(subfiles); + } + + getDirectories(path: string): string[] { + const p = this.resolve(path); + const subdirs = Object.keys(this._directories) + .filter(fileName => dirname(fileName) == p) + .map(path => basename(path)); + + let delegated: string[]; + try { + delegated = this._syncHost.list(p).filter((x: string) => { + try { + return this._syncHost.isDirectory(_join(p, x)); + } catch (e) { + return false; + } + }); + } catch (e) { + delegated = []; + } + + return delegated.concat(subdirs); + } + + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, _onError?: OnErrorFn) { + fileName = this.resolve(fileName); + + let stats = this._files[fileName]; + if (!stats) { + const content = this.readFile(fileName); + + if (!this._cache && content) { + return ts.createSourceFile( + workaroundResolve(fileName), + content, + languageVersion, + this._setParentNodes, + ); + } else { + stats = this._files[fileName]; + if (!stats) { + // If cache is turned on and the file exists, the readFile call will have populated stats. + // Empty stats at this point mean the file doesn't exist at and so we should return + // undefined. + return undefined; + } + } + } + + return stats && stats.getSourceFile(languageVersion, this._setParentNodes); + } + + get getCancellationToken() { + // return this._delegate.getCancellationToken; + // TODO: consider implementing a cancellation token. + return undefined; + } + + getDefaultLibFileName(options: ts.CompilerOptions) { + return ts.createCompilerHost(options, false).getDefaultLibFileName(options); + } + + // This is due to typescript CompilerHost interface being weird on writeFile. This shuts down + // typings in WebStorm. + get writeFile() { + return ( + fileName: string, + data: string, + _writeByteOrderMark: boolean, + _onError?: (message: string) => void, + _sourceFiles?: ReadonlyArray, + ): void => { + const p = this.resolve(fileName); + this._setFileContent(p, data); + }; + } + + getCurrentDirectory(): string { + return this._basePath !== null ? this._basePath : '/'; + } + + getCanonicalFileName(fileName: string): string { + return this.resolve(fileName); + } + + useCaseSensitiveFileNames(): boolean { + return !process.platform.startsWith('win32'); + } + + getNewLine(): string { + return '\n'; + } + + setResourceLoader(resourceLoader: WebpackResourceLoader) { + this._resourceLoader = resourceLoader; + } + + readResource(fileName: string) { + if (this._resourceLoader) { + // These paths are meant to be used by the loader so we must denormalize them. + const denormalizedFileName = this.denormalizePath(normalize(fileName)); + + return this._resourceLoader.get(denormalizedFileName); + } else { + return this.readFile(fileName); + } + } +} + + +// `TsCompilerAotCompilerTypeCheckHostAdapter` in @angular/compiler-cli seems to resolve module +// names directly via `resolveModuleName`, which prevents full Path usage. +// To work around this we must provide the same path format as TS internally uses in +// the SourceFile paths. +export function workaroundResolve(path: Path | string) { + return getSystemPath(normalize(path)).replace(/\\/g, '/'); +} diff --git a/packages/ngtools/webpack/src/entry_resolver.ts b/packages/ngtools/webpack/src/entry_resolver.ts new file mode 100644 index 0000000000..b844f96c5b --- /dev/null +++ b/packages/ngtools/webpack/src/entry_resolver.ts @@ -0,0 +1,168 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as fs from 'fs'; +import { join } from 'path'; +import * as ts from 'typescript'; +import { TypeScriptFileRefactor } from './refactor'; + + +function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor, + symbolName: string, + host: ts.CompilerHost, + program: ts.Program): string | null { + // Check this file. + const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) + .some((cd: ts.ClassDeclaration) => { + return cd.name != undefined && cd.name.text == symbolName; + }); + if (hasSymbol) { + return refactor.fileName; + } + + // We found the bootstrap variable, now we just need to get where it's imported. + const exports = refactor.findAstNodes(null, ts.SyntaxKind.ExportDeclaration) + .map(node => node as ts.ExportDeclaration); + + for (const decl of exports) { + if (!decl.moduleSpecifier || decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + continue; + } + + const modulePath = (decl.moduleSpecifier as ts.StringLiteral).text; + const resolvedModule = ts.resolveModuleName( + modulePath, refactor.fileName, program.getCompilerOptions(), host); + if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { + return null; + } + + const module = resolvedModule.resolvedModule.resolvedFileName; + if (!decl.exportClause) { + const moduleRefactor = new TypeScriptFileRefactor(module, host, program); + const maybeModule = _recursiveSymbolExportLookup(moduleRefactor, symbolName, host, program); + if (maybeModule) { + return maybeModule; + } + continue; + } + + const binding = decl.exportClause as ts.NamedExports; + for (const specifier of binding.elements) { + if (specifier.name.text == symbolName) { + // If it's a directory, load its index and recursively lookup. + if (fs.statSync(module).isDirectory()) { + const indexModule = join(module, 'index.ts'); + if (fs.existsSync(indexModule)) { + const indexRefactor = new TypeScriptFileRefactor(indexModule, host, program); + const maybeModule = _recursiveSymbolExportLookup( + indexRefactor, symbolName, host, program); + if (maybeModule) { + return maybeModule; + } + } + } + + // Create the source and verify that the symbol is at least a class. + const source = new TypeScriptFileRefactor(module, host, program); + const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration) + .some((cd: ts.ClassDeclaration) => { + return cd.name != undefined && cd.name.text == symbolName; + }); + + if (hasSymbol) { + return module; + } + } + } + } + + return null; +} + +function _symbolImportLookup(refactor: TypeScriptFileRefactor, + symbolName: string, + host: ts.CompilerHost, + program: ts.Program): string | null { + // We found the bootstrap variable, now we just need to get where it's imported. + const imports = refactor.findAstNodes(null, ts.SyntaxKind.ImportDeclaration) + .map(node => node as ts.ImportDeclaration); + + for (const decl of imports) { + if (!decl.importClause || !decl.moduleSpecifier) { + continue; + } + if (decl.moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + continue; + } + + const resolvedModule = ts.resolveModuleName( + (decl.moduleSpecifier as ts.StringLiteral).text, + refactor.fileName, program.getCompilerOptions(), host); + if (!resolvedModule.resolvedModule || !resolvedModule.resolvedModule.resolvedFileName) { + continue; + } + + const module = resolvedModule.resolvedModule.resolvedFileName; + if (decl.importClause.namedBindings + && decl.importClause.namedBindings.kind == ts.SyntaxKind.NamespaceImport) { + const binding = decl.importClause.namedBindings as ts.NamespaceImport; + if (binding.name.text == symbolName) { + // This is a default export. + return module; + } + } else if (decl.importClause.namedBindings + && decl.importClause.namedBindings.kind == ts.SyntaxKind.NamedImports) { + const binding = decl.importClause.namedBindings as ts.NamedImports; + for (const specifier of binding.elements) { + if (specifier.name.text == symbolName) { + // Create the source and recursively lookup the import. + const source = new TypeScriptFileRefactor(module, host, program); + const maybeModule = _recursiveSymbolExportLookup(source, symbolName, host, program); + if (maybeModule) { + return maybeModule; + } + } + } + } + } + + return null; +} + + +export function resolveEntryModuleFromMain(mainPath: string, + host: ts.CompilerHost, + program: ts.Program): string | null { + const source = new TypeScriptFileRefactor(mainPath, host, program); + + const bootstrap = source.findAstNodes(source.sourceFile, ts.SyntaxKind.CallExpression, true) + .map(node => node as ts.CallExpression) + .filter(call => { + const access = call.expression as ts.PropertyAccessExpression; + + return access.kind == ts.SyntaxKind.PropertyAccessExpression + && access.name.kind == ts.SyntaxKind.Identifier + && (access.name.text == 'bootstrapModule' + || access.name.text == 'bootstrapModuleFactory'); + }) + .map(node => node.arguments[0] as ts.Identifier) + .filter(node => node.kind == ts.SyntaxKind.Identifier); + + if (bootstrap.length != 1) { + return null; + } + const bootstrapSymbolName = bootstrap[0].text; + const module = _symbolImportLookup(source, bootstrapSymbolName, host, program); + if (module) { + return `${module.replace(/\.ts$/, '')}#${bootstrapSymbolName}`; + } + + // shrug... something bad happened and we couldn't find the import statement. + throw new Error('Tried to find bootstrap code, but could not. Specify either ' + + 'statically analyzable bootstrap code or pass in an entryModule ' + + 'to the plugins options.'); +} diff --git a/packages/ngtools/webpack/src/gather_diagnostics.ts b/packages/ngtools/webpack/src/gather_diagnostics.ts new file mode 100644 index 0000000000..6b6be8f745 --- /dev/null +++ b/packages/ngtools/webpack/src/gather_diagnostics.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { time, timeEnd } from './benchmark'; +import { Diagnostic, Diagnostics, Program } from './ngtools_api'; + + +export class CancellationToken implements ts.CancellationToken { + private _isCancelled = false; + + requestCancellation() { + this._isCancelled = true; + } + + isCancellationRequested() { + return this._isCancelled; + } + + throwIfCancellationRequested() { + if (this.isCancellationRequested()) { + throw new ts.OperationCanceledException(); + } + } +} + +export function hasErrors(diags: Diagnostics) { + return diags.some(d => d.category === ts.DiagnosticCategory.Error); +} + +export function gatherDiagnostics( + program: ts.Program | Program, + jitMode: boolean, + benchmarkLabel: string, + cancellationToken?: CancellationToken, +): Diagnostics { + const allDiagnostics: Array = []; + let checkOtherDiagnostics = true; + + function checkDiagnostics(fn: T) { + if (checkOtherDiagnostics) { + const diags = fn(undefined, cancellationToken); + if (diags) { + allDiagnostics.push(...diags); + + checkOtherDiagnostics = !hasErrors(diags); + } + } + } + + if (jitMode) { + const tsProgram = program as ts.Program; + // Check syntactic diagnostics. + time(`${benchmarkLabel}.gatherDiagnostics.ts.getSyntacticDiagnostics`); + checkDiagnostics(tsProgram.getSyntacticDiagnostics.bind(tsProgram)); + timeEnd(`${benchmarkLabel}.gatherDiagnostics.ts.getSyntacticDiagnostics`); + + // Check semantic diagnostics. + time(`${benchmarkLabel}.gatherDiagnostics.ts.getSemanticDiagnostics`); + checkDiagnostics(tsProgram.getSemanticDiagnostics.bind(tsProgram)); + timeEnd(`${benchmarkLabel}.gatherDiagnostics.ts.getSemanticDiagnostics`); + } else { + const angularProgram = program as Program; + + // Check TypeScript syntactic diagnostics. + time(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSyntacticDiagnostics`); + checkDiagnostics(angularProgram.getTsSyntacticDiagnostics.bind(angularProgram)); + timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSyntacticDiagnostics`); + + // Check TypeScript semantic and Angular structure diagnostics. + time(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSemanticDiagnostics`); + checkDiagnostics(angularProgram.getTsSemanticDiagnostics.bind(angularProgram)); + timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getTsSemanticDiagnostics`); + + // Check Angular semantic diagnostics + time(`${benchmarkLabel}.gatherDiagnostics.ng.getNgSemanticDiagnostics`); + checkDiagnostics(angularProgram.getNgSemanticDiagnostics.bind(angularProgram)); + timeEnd(`${benchmarkLabel}.gatherDiagnostics.ng.getNgSemanticDiagnostics`); + } + + return allDiagnostics; +} diff --git a/packages/ngtools/webpack/src/index.ts b/packages/ngtools/webpack/src/index.ts new file mode 100644 index 0000000000..0ac24959c8 --- /dev/null +++ b/packages/ngtools/webpack/src/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './angular_compiler_plugin'; +export { ngcLoader as default } from './loader'; diff --git a/packages/ngtools/webpack/src/lazy_routes.ts b/packages/ngtools/webpack/src/lazy_routes.ts new file mode 100644 index 0000000000..dc32ebcea8 --- /dev/null +++ b/packages/ngtools/webpack/src/lazy_routes.ts @@ -0,0 +1,100 @@ +/** + * @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 { dirname, join } from 'path'; +import * as ts from 'typescript'; +import { findAstNodes, resolve } from './refactor'; + + +function _getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null { + if (node.kind == ts.SyntaxKind.Identifier) { + return (node as ts.Identifier).text; + } else if (node.kind == ts.SyntaxKind.StringLiteral) { + return (node as ts.StringLiteral).text; + } else { + return null; + } +} + + +export interface LazyRouteMap { + [path: string]: string; +} + + +export function findLazyRoutes( + filePath: string, + host: ts.CompilerHost, + program?: ts.Program, + compilerOptions?: ts.CompilerOptions, +): LazyRouteMap { + if (!compilerOptions) { + if (!program) { + throw new Error('Must pass either program or compilerOptions to findLazyRoutes.'); + } + compilerOptions = program.getCompilerOptions(); + } + const fileName = resolve(filePath, host, compilerOptions).replace(/\\/g, '/'); + let sourceFile: ts.SourceFile | undefined; + if (program) { + sourceFile = program.getSourceFile(fileName); + } + + if (!sourceFile) { + const content = host.readFile(fileName); + if (content) { + sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true); + } + } + + if (!sourceFile) { + throw new Error(`Source file not found: '${fileName}'.`); + } + const sf: ts.SourceFile = sourceFile; + + return findAstNodes(null, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true) + // Get all their property assignments. + .map((node: ts.ObjectLiteralExpression) => { + return findAstNodes(node, sf, ts.SyntaxKind.PropertyAssignment, false); + }) + // Take all `loadChildren` elements. + .reduce((acc: ts.PropertyAssignment[], props: ts.PropertyAssignment[]) => { + return acc.concat(props.filter(literal => { + return _getContentOfKeyLiteral(sf, literal.name) == 'loadChildren'; + })); + }, []) + // Get only string values. + .filter((node: ts.PropertyAssignment) => node.initializer.kind == ts.SyntaxKind.StringLiteral) + // Get the string value. + .map((node: ts.PropertyAssignment) => (node.initializer as ts.StringLiteral).text) + // Map those to either [path, absoluteModulePath], or [path, null] if the module pointing to + // does not exist. + .map((routePath: string) => { + const moduleName = routePath.split('#')[0]; + const compOptions = (program && program.getCompilerOptions()) || compilerOptions || {}; + const resolvedModuleName: ts.ResolvedModuleWithFailedLookupLocations = moduleName[0] == '.' + ? ({ + resolvedModule: { resolvedFileName: join(dirname(filePath), moduleName) + '.ts' }, + } as ts.ResolvedModuleWithFailedLookupLocations) + : ts.resolveModuleName(moduleName, filePath, compOptions, host); + if (resolvedModuleName.resolvedModule + && resolvedModuleName.resolvedModule.resolvedFileName + && host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) { + return [routePath, resolvedModuleName.resolvedModule.resolvedFileName]; + } else { + return [routePath, null]; + } + }) + // Reduce to the LazyRouteMap map. + .reduce((acc: LazyRouteMap, [routePath, resolvedModuleName]: [string, string | null]) => { + if (resolvedModuleName) { + acc[routePath] = resolvedModuleName; + } + + return acc; + }, {}); +} diff --git a/packages/ngtools/webpack/src/loader.ts b/packages/ngtools/webpack/src/loader.ts new file mode 100644 index 0000000000..ca6afad079 --- /dev/null +++ b/packages/ngtools/webpack/src/loader.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// TODO: fix typings. +// tslint:disable-next-line:no-global-tslint-disable +// tslint:disable:no-any +import * as path from 'path'; +import { loader } from 'webpack'; +import { AngularCompilerPlugin } from './angular_compiler_plugin'; +import { time, timeEnd } from './benchmark'; + + +const sourceMappingUrlRe = /^\/\/# sourceMappingURL=[^\r\n]*/gm; + +export function ngcLoader(this: loader.LoaderContext) { + const cb = this.async(); + const sourceFileName: string = this.resourcePath; + const timeLabel = `ngcLoader+${sourceFileName}+`; + + if (!cb) { + throw new Error('This loader needs to support asynchronous webpack compilations.'); + } + + time(timeLabel); + + const plugin = this._compilation._ngToolsWebpackPluginInstance; + if (!plugin) { + throw new Error('The AngularCompilerPlugin was not found. ' + + 'The @ngtools/webpack loader requires the plugin.'); + } + + // We must verify that the plugin is an instance of the right class. + // Throw an error if it isn't, that often means multiple @ngtools/webpack installs. + if (!(plugin instanceof AngularCompilerPlugin) || !plugin.done) { + throw new Error('Angular Compiler was detected but it was an instance of the wrong class.\n' + + 'This likely means you have several @ngtools/webpack packages installed. ' + + 'You can check this with `npm ls @ngtools/webpack`, and then remove the extra copies.', + ); + } + + time(timeLabel + '.ngcLoader.AngularCompilerPlugin'); + plugin.done + .then(() => { + timeEnd(timeLabel + '.ngcLoader.AngularCompilerPlugin'); + const result = plugin.getCompiledFile(sourceFileName); + + if (result.sourceMap) { + // Process sourcemaps for Webpack. + // Remove the sourceMappingURL. + result.outputText = result.outputText.replace(sourceMappingUrlRe, ''); + // Set the map source to use the full path of the file. + const sourceMap = JSON.parse(result.sourceMap); + sourceMap.sources = sourceMap.sources.map((fileName: string) => { + return path.join(path.dirname(sourceFileName), fileName); + }); + result.sourceMap = sourceMap; + } + + // Manually add the dependencies for TS files. + // Type only imports will be stripped out by compilation so we need to add them as + // as dependencies. + // Component resources files (html and css templates) also need to be added manually for + // AOT, so that this file is reloaded when they change. + if (sourceFileName.endsWith('.ts')) { + result.errorDependencies.forEach(dep => this.addDependency(dep)); + const dependencies = plugin.getDependencies(sourceFileName); + dependencies.forEach(dep => { + plugin.updateChangedFileExtensions(path.extname(dep)); + this.addDependency(dep); + }); + } + + // NgFactory files depend on the component template, but we can't know what that file + // is (if any). So we add all the dependencies that the original component file has + // to the factory as well, which includes html and css templates, and the component + // itself (for inline html/templates templates). + const ngFactoryRe = /\.ngfactory.js$/; + if (ngFactoryRe.test(sourceFileName)) { + const originalFile = sourceFileName.replace(ngFactoryRe, '.ts'); + this.addDependency(originalFile); + const origDependencies = plugin.getDependencies(originalFile); + origDependencies.forEach(dep => this.addDependency(dep)); + } + + // NgStyle files depend on the style file they represent. + // E.g. `some-style.less.shim.ngstyle.js` depends on `some-style.less`. + // Those files can in turn depend on others, so we have to add them all. + const ngStyleRe = /(?:\.shim)?\.ngstyle\.js$/; + if (ngStyleRe.test(sourceFileName)) { + const styleFile = sourceFileName.replace(ngStyleRe, ''); + const styleDependencies = plugin.getResourceDependencies(styleFile); + styleDependencies.forEach(dep => this.addDependency(dep)); + } + + timeEnd(timeLabel); + cb(null, result.outputText, result.sourceMap as any); + }) + .catch(err => { + timeEnd(timeLabel); + cb(err); + }); +} diff --git a/packages/ngtools/webpack/src/ngtools_api.ts b/packages/ngtools/webpack/src/ngtools_api.ts new file mode 100644 index 0000000000..3bee59dd64 --- /dev/null +++ b/packages/ngtools/webpack/src/ngtools_api.ts @@ -0,0 +1,107 @@ +/** + * @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 + */ +// We disable implicit dependenccies because those are only for typings and don't have a runtime +// equivalent. +// tslint:disable-next-line:no-global-tslint-disable +// tslint:disable:no-implicit-dependencies +/** + * This is a copy of types in @compiler-cli/src/ngtools_api.d.ts file, + * together with safe imports for private apis for cases where @angular/compiler-cli isn't + * available or is below version 5. + */ +import * as ngc from '@angular/compiler-cli'; +import * as ngtools from '@angular/compiler-cli/ngtools2'; +import * as path from 'path'; +import * as ts from 'typescript'; + +export const DEFAULT_ERROR_CODE = 100; +export const UNKNOWN_ERROR_CODE = 500; +export const SOURCE = 'angular' as 'angular'; + +export type CompilerOptions = ngc.CompilerOptions; +export type CompilerHost = ngtools.CompilerHost; +export type Program = ngtools.Program; +export type Diagnostic = ngtools.Diagnostic; +export type Diagnostics = ReadonlyArray; + +function _error(api: string, fn: string): never { + throw new Error('Could not find API ' + api + ', function ' + fn); +} + +// Manually check for Compiler CLI availability and supported version. +// This is needed because @ngtools/webpack does not depend directly on @angular/compiler-cli, since +// it is installed as part of global Angular CLI installs and compiler-cli is not of its +// dependencies. +export function CompilerCliIsSupported() { + let version; + + // Check that Angular is available. + try { + version = (require('@angular/compiler-cli') as typeof ngc).VERSION; + } catch (e) { + throw new Error('The "@angular/compiler-cli" package was not properly installed. Error: ' + e); + } + + // Check that Angular is also not part of this module's node_modules (it should be the project's). + const compilerCliPath = require.resolve('@angular/compiler-cli'); + if (compilerCliPath.startsWith(path.dirname(__dirname))) { + throw new Error('The @ngtools/webpack plugin now relies on the project @angular/compiler-cli. ' + + 'Please clean your node_modules and reinstall.'); + } + + // Throw if we're less than 5.x + if (Number(version.major) < 5) { + throw new Error('Version of @angular/compiler-cli needs to be 5.0.0 or greater. ' + + `Current version is "${version.full}".`); + } +} + +// These imports do not exist on a global install for Angular CLI, so we cannot use a static ES6 +// import. +let compilerCli: typeof ngc | null = null; +try { + compilerCli = require('@angular/compiler-cli'); +} catch { + // Don't throw an error if the private API does not exist. + // Instead, the `CompilerCliIsSupported` method should return throw and indicate the + // plugin cannot be used. +} + +export const VERSION: typeof ngc.VERSION = + compilerCli + && compilerCli.VERSION + || _error('compiler-cli', 'VERSION'); +export const __NGTOOLS_PRIVATE_API_2: typeof ngc.__NGTOOLS_PRIVATE_API_2 = + compilerCli + && compilerCli.__NGTOOLS_PRIVATE_API_2 + || _error('compiler-cli', '__NGTOOLS_PRIVATE_API_2'); +export const readConfiguration: typeof ngc.readConfiguration = + compilerCli + && compilerCli.readConfiguration + || _error('compiler-cli', 'readConfiguration'); + + +// These imports do not exist on Angular versions lower than 5, so we cannot use a static ES6 +// import. +let ngtools2: typeof ngtools | null = null; +try { + ngtools2 = require('@angular/compiler-cli/ngtools2'); +} catch { + // Don't throw an error if the private API does not exist. + // Instead, the `AngularCompilerPlugin.isSupported` method should return false and indicate the + // plugin cannot be used. +} + +export const createProgram: typeof ngtools.createProgram = + ngtools2 && ngtools2.createProgram || _error('ngtools2', 'createProgram'); +export const createCompilerHost: typeof ngtools.createCompilerHost = + ngtools2 && ngtools2.createCompilerHost || _error('ngtools2', 'createCompilerHost'); +export const formatDiagnostics: typeof ngtools.formatDiagnostics = + ngtools2 && ngtools2.formatDiagnostics || _error('ngtools2', 'formatDiagnostics'); +export const EmitFlags: typeof ngtools.EmitFlags = + ngtools2 && ngtools2.EmitFlags || _error('ngtools', 'EmitFlags'); diff --git a/packages/ngtools/webpack/src/paths-plugin.ts b/packages/ngtools/webpack/src/paths-plugin.ts new file mode 100644 index 0000000000..846648d0ae --- /dev/null +++ b/packages/ngtools/webpack/src/paths-plugin.ts @@ -0,0 +1,142 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as path from 'path'; +import * as ts from 'typescript'; +import { + Callback, + NormalModuleFactoryRequest, +} from './webpack'; + + +export function resolveWithPaths( + request: NormalModuleFactoryRequest, + callback: Callback, + compilerOptions: ts.CompilerOptions, + host: ts.CompilerHost, + cache?: ts.ModuleResolutionCache, +) { + if (!request || !request.request || !compilerOptions.paths) { + callback(null, request); + + return; + } + + // Only work on Javascript/TypeScript issuers. + if (!request.contextInfo.issuer || !request.contextInfo.issuer.match(/\.[jt]s$/)) { + callback(null, request); + + return; + } + + const originalRequest = request.request.trim(); + + // Relative requests are not mapped + if (originalRequest.startsWith('.') || originalRequest.startsWith('/')) { + callback(null, request); + + return; + } + + // check if any path mapping rules are relevant + const pathMapOptions = []; + for (const pattern in compilerOptions.paths) { + // can only contain zero or one + const starIndex = pattern.indexOf('*'); + if (starIndex === -1) { + if (pattern === originalRequest) { + pathMapOptions.push({ + partial: '', + potentials: compilerOptions.paths[pattern], + }); + } + } else if (starIndex === 0 && pattern.length === 1) { + pathMapOptions.push({ + partial: originalRequest, + potentials: compilerOptions.paths[pattern], + }); + } else if (starIndex === pattern.length - 1) { + if (originalRequest.startsWith(pattern.slice(0, -1))) { + pathMapOptions.push({ + partial: originalRequest.slice(pattern.length - 1), + potentials: compilerOptions.paths[pattern], + }); + } + } else { + const [prefix, suffix] = pattern.split('*'); + if (originalRequest.startsWith(prefix) && originalRequest.endsWith(suffix)) { + pathMapOptions.push({ + partial: originalRequest.slice(prefix.length).slice(0, -suffix.length), + potentials: compilerOptions.paths[pattern], + }); + } + } + } + + if (pathMapOptions.length === 0) { + callback(null, request); + + return; + } + + if (pathMapOptions.length === 1 && pathMapOptions[0].potentials.length === 1) { + const onlyPotential = pathMapOptions[0].potentials[0]; + let replacement; + const starIndex = onlyPotential.indexOf('*'); + if (starIndex === -1) { + replacement = onlyPotential; + } else if (starIndex === onlyPotential.length - 1) { + replacement = onlyPotential.slice(0, -1) + pathMapOptions[0].partial; + } else { + const [prefix, suffix] = onlyPotential.split('*'); + replacement = prefix + pathMapOptions[0].partial + suffix; + } + + request.request = path.resolve(compilerOptions.baseUrl || '', replacement); + callback(null, request); + + return; + } + + const moduleResolver = ts.resolveModuleName( + originalRequest, + request.contextInfo.issuer, + compilerOptions, + host, + cache, + ); + + const moduleFilePath = moduleResolver.resolvedModule + && moduleResolver.resolvedModule.resolvedFileName; + + // If there is no result, let webpack try to resolve + if (!moduleFilePath) { + callback(null, request); + + return; + } + + // If TypeScript gives us a `.d.ts`, it is probably a node module + if (moduleFilePath.endsWith('.d.ts')) { + // If in a package, let webpack resolve the package + const packageRootPath = path.join(path.dirname(moduleFilePath), 'package.json'); + if (!host.fileExists(packageRootPath)) { + // Otherwise, if there is a file with a .js extension use that + const jsFilePath = moduleFilePath.slice(0, -5) + '.js'; + if (host.fileExists(jsFilePath)) { + request.request = jsFilePath; + } + } + + callback(null, request); + + return; + } + + request.request = moduleFilePath; + callback(null, request); +} diff --git a/packages/ngtools/webpack/src/refactor.ts b/packages/ngtools/webpack/src/refactor.ts new file mode 100644 index 0000000000..634117268d --- /dev/null +++ b/packages/ngtools/webpack/src/refactor.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as path from 'path'; +import * as ts from 'typescript'; + + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node The root node to check, or null if the whole tree should be searched. + * @param sourceFile The source file where the node is. + * @param kind The kind of nodes to find. + * @param recursive Whether to go in matched nodes to keep matching. + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +// TODO: replace this with collectDeepNodes and add limits to collectDeepNodes +export function findAstNodes( + node: ts.Node | null, + sourceFile: ts.SourceFile, + kind: ts.SyntaxKind, + recursive = false, + max = Infinity, +): T[] { + // TODO: refactor operations that only need `refactor.findAstNodes()` to use this instead. + if (max == 0) { + return []; + } + if (!node) { + node = sourceFile; + } + + const arr: T[] = []; + if (node.kind === kind) { + // If we're not recursively looking for children, stop here. + if (!recursive) { + return [node as T]; + } + + arr.push(node as T); + max--; + } + + if (max > 0) { + for (const child of node.getChildren(sourceFile)) { + findAstNodes(child, sourceFile, kind, recursive, max) + .forEach((node: ts.Node) => { + if (max > 0) { + arr.push(node as T); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +export function resolve( + filePath: string, + _host: ts.CompilerHost, + compilerOptions: ts.CompilerOptions, +): string { + if (path.isAbsolute(filePath)) { + return filePath; + } + const basePath = compilerOptions.baseUrl || compilerOptions.rootDir; + if (!basePath) { + throw new Error(`Trying to resolve '${filePath}' without a basePath.`); + } + + return path.join(basePath, filePath); +} + + +export class TypeScriptFileRefactor { + private _fileName: string; + private _sourceFile: ts.SourceFile; + + get fileName() { return this._fileName; } + get sourceFile() { return this._sourceFile; } + + constructor(fileName: string, + _host: ts.CompilerHost, + _program?: ts.Program, + source?: string | null) { + let sourceFile: ts.SourceFile | null = null; + + if (_program) { + fileName = resolve(fileName, _host, _program.getCompilerOptions()).replace(/\\/g, '/'); + this._fileName = fileName; + + if (source) { + sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); + } else { + sourceFile = _program.getSourceFile(fileName) || null; + } + } + if (!sourceFile) { + const maybeContent = source || _host.readFile(fileName); + if (maybeContent) { + sourceFile = ts.createSourceFile( + fileName, + maybeContent, + ts.ScriptTarget.Latest, + true, + ); + } + } + if (!sourceFile) { + throw new Error('Must have a source file to refactor.'); + } + + this._sourceFile = sourceFile; + } + + /** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node The root node to check, or null if the whole tree should be searched. + * @param kind The kind of nodes to find. + * @param recursive Whether to go in matched nodes to keep matching. + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ + findAstNodes(node: ts.Node | null, + kind: ts.SyntaxKind, + recursive = false, + max = Infinity): ts.Node[] { + return findAstNodes(node, this._sourceFile, kind, recursive, max); + } + +} diff --git a/packages/ngtools/webpack/src/resource_loader.ts b/packages/ngtools/webpack/src/resource_loader.ts new file mode 100644 index 0000000000..7535865677 --- /dev/null +++ b/packages/ngtools/webpack/src/resource_loader.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// TODO: fix typings. +// tslint:disable-next-line:no-global-tslint-disable +// tslint:disable:no-any +import * as path from 'path'; +import * as vm from 'vm'; +import { RawSource } from 'webpack-sources'; + +const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); +const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); +const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); +const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); + + +interface CompilationOutput { + outputName: string; + source: string; +} + +export class WebpackResourceLoader { + private _parentCompilation: any; + private _context: string; + private _fileDependencies = new Map(); + private _cachedSources = new Map(); + private _cachedEvaluatedSources = new Map(); + + constructor() {} + + update(parentCompilation: any) { + this._parentCompilation = parentCompilation; + this._context = parentCompilation.context; + } + + getResourceDependencies(filePath: string) { + return this._fileDependencies.get(filePath) || []; + } + + private _compile(filePath: string): Promise { + + if (!this._parentCompilation) { + throw new Error('WebpackResourceLoader cannot be used without parentCompilation'); + } + + // Simple sanity check. + if (filePath.match(/\.[jt]s$/)) { + return Promise.reject('Cannot use a JavaScript or TypeScript file for styleUrl.'); + } + + const outputOptions = { filename: filePath }; + const relativePath = path.relative(this._context || '', filePath); + const childCompiler = this._parentCompilation.createChildCompiler(relativePath, outputOptions); + childCompiler.context = this._context; + + new NodeTemplatePlugin(outputOptions).apply(childCompiler); + new NodeTargetPlugin().apply(childCompiler); + new SingleEntryPlugin(this._context, filePath).apply(childCompiler); + new LoaderTargetPlugin('node').apply(childCompiler); + + childCompiler.hooks.thisCompilation.tap('ngtools-webpack', (compilation: any) => { + compilation.hooks.additionalAssets.tapAsync('ngtools-webpack', + (callback: (err?: Error) => void) => { + if (this._cachedEvaluatedSources.has(compilation.fullHash)) { + const cachedEvaluatedSource = this._cachedEvaluatedSources.get(compilation.fullHash); + compilation.assets[filePath] = cachedEvaluatedSource; + callback(); + + return; + } + + const asset = compilation.assets[filePath]; + if (asset) { + this._evaluate({ outputName: filePath, source: asset.source() }) + .then(output => { + const evaluatedSource = new RawSource(output); + this._cachedEvaluatedSources.set(compilation.fullHash, evaluatedSource); + compilation.assets[filePath] = evaluatedSource; + callback(); + }) + .catch(err => callback(err)); + } else { + callback(); + } + }); + }); + + // Compile and return a promise + return new Promise((resolve, reject) => { + childCompiler.compile((err: Error, childCompilation: any) => { + // Resolve / reject the promise + if (childCompilation && childCompilation.errors && childCompilation.errors.length) { + const errorDetails = childCompilation.errors.map(function (error: any) { + return error.message + (error.error ? ':\n' + error.error : ''); + }).join('\n'); + reject(new Error('Child compilation failed:\n' + errorDetails)); + } else if (err) { + reject(err); + } else { + Object.keys(childCompilation.assets).forEach(assetName => { + if (assetName !== filePath && this._parentCompilation.assets[assetName] == undefined) { + this._parentCompilation.assets[assetName] = childCompilation.assets[assetName]; + } + }); + + // Save the dependencies for this resource. + this._fileDependencies.set(filePath, childCompilation.fileDependencies); + + const compilationHash = childCompilation.fullHash; + const maybeSource = this._cachedSources.get(compilationHash); + if (maybeSource) { + resolve({ outputName: filePath, source: maybeSource }); + } else { + const source = childCompilation.assets[filePath].source(); + this._cachedSources.set(compilationHash, source); + + resolve({ outputName: filePath, source }); + } + } + }); + }); + } + + private _evaluate({ outputName, source }: CompilationOutput): Promise { + try { + // Evaluate code + const evaluatedSource = vm.runInNewContext(source, undefined, { filename: outputName }); + + if (typeof evaluatedSource == 'string') { + return Promise.resolve(evaluatedSource); + } + + return Promise.reject('The loader "' + outputName + '" didn\'t return a string.'); + } catch (e) { + return Promise.reject(e); + } + } + + get(filePath: string): Promise { + return this._compile(filePath) + .then((result: CompilationOutput) => result.source); + } +} diff --git a/packages/ngtools/webpack/src/transformers/ast_helpers.ts b/packages/ngtools/webpack/src/transformers/ast_helpers.ts new file mode 100644 index 0000000000..83c4bfdbaf --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/ast_helpers.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { WebpackCompilerHost } from '../compiler_host'; + + +// Find all nodes from the AST in the subtree of node of SyntaxKind kind. +export function collectDeepNodes(node: ts.Node, kind: ts.SyntaxKind): T[] { + const nodes: T[] = []; + const helper = (child: ts.Node) => { + if (child.kind === kind) { + nodes.push(child as T); + } + ts.forEachChild(child, helper); + }; + ts.forEachChild(node, helper); + + return nodes; +} + +export function getFirstNode(sourceFile: ts.SourceFile): ts.Node { + if (sourceFile.statements.length > 0) { + return sourceFile.statements[0]; + } + + return sourceFile.getChildAt(0); +} + +export function getLastNode(sourceFile: ts.SourceFile): ts.Node | null { + if (sourceFile.statements.length > 0) { + return sourceFile.statements[sourceFile.statements.length - 1] || null; + } + + return null; +} + + +// Test transform helpers. +const basePath = '/project/src/'; +const fileName = basePath + 'test-file.ts'; + +export function createTypescriptContext(content: string) { + // Set compiler options. + const compilerOptions: ts.CompilerOptions = { + noEmitOnError: false, + allowJs: true, + newLine: ts.NewLineKind.LineFeed, + target: ts.ScriptTarget.ESNext, + skipLibCheck: true, + sourceMap: false, + importHelpers: true, + }; + + // Create compiler host. + const compilerHost = new WebpackCompilerHost(compilerOptions, basePath); + + // Add a dummy file to host content. + compilerHost.writeFile(fileName, content, false); + + // Create the TypeScript program. + const program = ts.createProgram([fileName], compilerOptions, compilerHost); + + return { compilerHost, program }; +} + +export function transformTypescript( + content: string | undefined, + transformers: ts.TransformerFactory[], + program?: ts.Program, + compilerHost?: WebpackCompilerHost, +) { + + // Use given context or create a new one. + if (content !== undefined) { + const typescriptContext = createTypescriptContext(content); + program = typescriptContext.program; + compilerHost = typescriptContext.compilerHost; + } else if (!program || !compilerHost) { + throw new Error('transformTypescript needs either `content` or a `program` and `compilerHost'); + } + + // Emit. + const { emitSkipped, diagnostics } = program.emit( + undefined, undefined, undefined, undefined, { before: transformers }, + ); + + // Log diagnostics if emit wasn't successfull. + if (emitSkipped) { + console.log(diagnostics); + + return null; + } + + // Return the transpiled js. + return compilerHost.readFile(fileName.replace(/\.ts$/, '.js')); +} diff --git a/packages/ngtools/webpack/src/transformers/elide_imports.ts b/packages/ngtools/webpack/src/transformers/elide_imports.ts new file mode 100644 index 0000000000..0bdd9dea6d --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/elide_imports.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { RemoveNodeOperation, TransformOperation } from './interfaces'; + + +// Remove imports for which all identifiers have been removed. +// Needs type checker, and works even if it's not the first transformer. +// Works by removing imports for symbols whose identifiers have all been removed. +// Doesn't use the `symbol.declarations` because that previous transforms might have removed nodes +// but the type checker doesn't know. +// See https://github.com/Microsoft/TypeScript/issues/17552 for more information. +export function elideImports( + sourceFile: ts.SourceFile, + removedNodes: ts.Node[], + getTypeChecker: () => ts.TypeChecker, +): TransformOperation[] { + const ops: TransformOperation[] = []; + + if (removedNodes.length === 0) { + return []; + } + + const typeChecker = getTypeChecker(); + + // Collect all imports and used identifiers + const specialCaseNames = new Set(); + const usedSymbols = new Set(); + const imports = [] as ts.ImportDeclaration[]; + ts.forEachChild(sourceFile, function visit(node) { + // Skip removed nodes + if (removedNodes.includes(node)) { + return; + } + + // Record import and skip + if (ts.isImportDeclaration(node)) { + imports.push(node); + + return; + } + + if (ts.isIdentifier(node)) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + usedSymbols.add(symbol); + } + } else if (ts.isExportSpecifier(node)) { + // Export specifiers return the non-local symbol from the above + // so check the name string instead + specialCaseNames.add((node.propertyName || node.name).text); + + return; + } else if (ts.isShorthandPropertyAssignment(node)) { + // Shorthand property assignments return the object property's symbol not the import's + specialCaseNames.add(node.name.text); + } + + ts.forEachChild(node, visit); + }); + + if (imports.length === 0) { + return []; + } + + const isUnused = (node: ts.Identifier) => { + if (specialCaseNames.has(node.text)) { + return false; + } + + const symbol = typeChecker.getSymbolAtLocation(node); + + return symbol && !usedSymbols.has(symbol); + }; + + for (const node of imports) { + if (!node.importClause) { + // "import 'abc';" + continue; + } + + if (node.importClause.name) { + // "import XYZ from 'abc';" + if (isUnused(node.importClause.name)) { + ops.push(new RemoveNodeOperation(sourceFile, node)); + } + } else if (node.importClause.namedBindings + && ts.isNamespaceImport(node.importClause.namedBindings)) { + // "import * as XYZ from 'abc';" + if (isUnused(node.importClause.namedBindings.name)) { + ops.push(new RemoveNodeOperation(sourceFile, node)); + } + } else if (node.importClause.namedBindings + && ts.isNamedImports(node.importClause.namedBindings)) { + // "import { XYZ, ... } from 'abc';" + const specifierOps = []; + for (const specifier of node.importClause.namedBindings.elements) { + if (isUnused(specifier.propertyName || specifier.name)) { + specifierOps.push(new RemoveNodeOperation(sourceFile, specifier)); + } + } + + if (specifierOps.length === node.importClause.namedBindings.elements.length) { + ops.push(new RemoveNodeOperation(sourceFile, node)); + } else { + ops.push(...specifierOps); + } + } + } + + return ops; +} diff --git a/packages/ngtools/webpack/src/transformers/export_lazy_module_map.ts b/packages/ngtools/webpack/src/transformers/export_lazy_module_map.ts new file mode 100644 index 0000000000..9947b3179e --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/export_lazy_module_map.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as path from 'path'; +import * as ts from 'typescript'; +import { LazyRouteMap } from '../lazy_routes'; +import { getFirstNode, getLastNode } from './ast_helpers'; +import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + +export function exportLazyModuleMap( + shouldTransform: (fileName: string) => boolean, + lazyRoutesCb: () => LazyRouteMap, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + const lazyRoutes = lazyRoutesCb(); + + if (!shouldTransform(sourceFile.fileName)) { + return ops; + } + + const dirName = path.normalize(path.dirname(sourceFile.fileName)); + + const modules = Object.keys(lazyRoutes) + .map((loadChildrenString) => { + const [, moduleName] = loadChildrenString.split('#'); + const modulePath = lazyRoutes[loadChildrenString]; + + return { + modulePath, + moduleName, + loadChildrenString, + }; + }); + + modules.forEach((module, index) => { + const modulePath = module.modulePath; + if (!modulePath) { + return; + } + + const relativePath = path.relative(dirName, modulePath).replace(/\\/g, '/'); + // Create the new namespace import node. + const namespaceImport = ts.createNamespaceImport(ts.createIdentifier(`__lazy_${index}__`)); + const importClause = ts.createImportClause(undefined, namespaceImport); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(relativePath)); + + const firstNode = getFirstNode(sourceFile); + if (firstNode) { + ops.push(new AddNodeOperation(sourceFile, firstNode, newImport)); + } + }); + + const lazyModuleObjectLiteral = ts.createObjectLiteral( + modules.map((mod, idx) => { + let [modulePath, moduleName] = mod.loadChildrenString.split('#'); + if (modulePath.match(/\.ngfactory/)) { + modulePath = modulePath.replace('.ngfactory', ''); + moduleName = moduleName.replace('NgFactory', ''); + } + + return ts.createPropertyAssignment( + ts.createLiteral(`${modulePath}#${moduleName}`), + ts.createPropertyAccess(ts.createIdentifier(`__lazy_${idx}__`), mod.moduleName)); + }), + ); + + const lazyModuleVariableStmt = ts.createVariableStatement( + [ts.createToken(ts.SyntaxKind.ExportKeyword)], + [ts.createVariableDeclaration('LAZY_MODULE_MAP', undefined, lazyModuleObjectLiteral)], + ); + + const lastNode = getLastNode(sourceFile); + if (lastNode) { + ops.push(new AddNodeOperation(sourceFile, lastNode, undefined, lazyModuleVariableStmt)); + } + + return ops; + }; + + return makeTransform(standardTransform); +} diff --git a/packages/ngtools/webpack/src/transformers/export_lazy_module_map_spec.ts b/packages/ngtools/webpack/src/transformers/export_lazy_module_map_spec.ts new file mode 100644 index 0000000000..0091d4659e --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/export_lazy_module_map_spec.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { transformTypescript } from './ast_helpers'; +import { exportLazyModuleMap } from './export_lazy_module_map'; + +describe('@ngtools/webpack transformers', () => { + describe('export_lazy_module_map', () => { + it('should create module map for JIT', () => { + const input = tags.stripIndent` + export { AppModule } from './app/app.module'; + `; + // tslint:disable:max-line-length + const output = tags.stripIndent` + import * as __lazy_0__ from "app/lazy/lazy.module.ts"; + import * as __lazy_1__ from "app/lazy2/lazy2.module.ts"; + export { AppModule } from './app/app.module'; + export var LAZY_MODULE_MAP = { "./lazy/lazy.module#LazyModule": __lazy_0__.LazyModule, "./lazy2/lazy2.module#LazyModule2": __lazy_1__.LazyModule2 }; + `; + // tslint:enable:max-line-length + + const transformer = exportLazyModuleMap( + () => true, + () => ({ + './lazy/lazy.module#LazyModule': '/project/src/app/lazy/lazy.module.ts', + './lazy2/lazy2.module#LazyModule2': '/project/src/app/lazy2/lazy2.module.ts', + }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should create module map for AOT', () => { + const input = tags.stripIndent` + export { AppModule } from './app/app.module'; + `; + // tslint:disable:max-line-length + const expected = tags.stripIndent` + import * as __lazy_0__ from "app/lazy/lazy.module.ngfactory.ts"; + import * as __lazy_1__ from "app/lazy2/lazy2.module.ngfactory.ts"; + export { AppModule } from './app/app.module'; + export var LAZY_MODULE_MAP = { "./lazy/lazy.module#LazyModule": __lazy_0__.LazyModuleNgFactory, "./lazy2/lazy2.module#LazyModule2": __lazy_1__.LazyModule2NgFactory }; + `; + // tslint:enable:max-line-length + + const transformer = exportLazyModuleMap( + () => true, + () => ({ + './lazy/lazy.module.ngfactory#LazyModuleNgFactory': + '/project/src/app/lazy/lazy.module.ngfactory.ts', + './lazy2/lazy2.module.ngfactory#LazyModule2NgFactory': + '/project/src/app/lazy2/lazy2.module.ngfactory.ts', + }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${expected}`); + }); + }); + + it('should not do anything if shouldTransform returns false', () => { + const input = tags.stripIndent` + export { AppModule } from './app/app.module'; + `; + + const transformer = exportLazyModuleMap( + () => false, + () => ({ + './lazy/lazy.module#LazyModule': '/project/src/app/lazy/lazy.module.ts', + './lazy2/lazy2.module#LazyModule2': '/project/src/app/lazy2/lazy2.module.ts', + }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/export_ngfactory.ts b/packages/ngtools/webpack/src/transformers/export_ngfactory.ts new file mode 100644 index 0000000000..04a8afd1bc --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/export_ngfactory.ts @@ -0,0 +1,79 @@ +/** + * @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 { dirname, relative } from 'path'; +import * as ts from 'typescript'; +import { collectDeepNodes, getFirstNode } from './ast_helpers'; +import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + +export function exportNgFactory( + shouldTransform: (fileName: string) => boolean, + getEntryModule: () => { path: string, className: string } | null, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + const entryModule = getEntryModule(); + + if (!shouldTransform(sourceFile.fileName) || !entryModule) { + return ops; + } + + // Find all identifiers using the entry module class name. + const entryModuleIdentifiers = collectDeepNodes(sourceFile, + ts.SyntaxKind.Identifier) + .filter(identifier => identifier.text === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); + const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/'); + + // Get the module path from the import. + entryModuleIdentifiers.forEach((entryModuleIdentifier) => { + if (!entryModuleIdentifier.parent + || entryModuleIdentifier.parent.kind !== ts.SyntaxKind.ExportSpecifier) { + return; + } + + const exportSpec = entryModuleIdentifier.parent as ts.ExportSpecifier; + const moduleSpecifier = exportSpec.parent + && exportSpec.parent.parent + && exportSpec.parent.parent.moduleSpecifier; + + if (!moduleSpecifier || moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { + return; + } + + // Add the transform operations. + const factoryClassName = entryModule.className + 'NgFactory'; + const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; + + const namedExports = ts.createNamedExports([ts.createExportSpecifier(undefined, + ts.createIdentifier(factoryClassName))]); + const newImport = ts.createExportDeclaration(undefined, undefined, namedExports, + ts.createLiteral(factoryModulePath)); + + const firstNode = getFirstNode(sourceFile); + if (firstNode) { + ops.push(new AddNodeOperation( + sourceFile, + firstNode, + newImport, + )); + } + }); + + return ops; + }; + + return makeTransform(standardTransform); +} diff --git a/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts b/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts new file mode 100644 index 0000000000..52dc70c098 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/export_ngfactory_spec.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { transformTypescript } from './ast_helpers'; +import { exportNgFactory } from './export_ngfactory'; + +describe('@ngtools/webpack transformers', () => { + describe('export_ngfactory', () => { + it('should export the ngfactory', () => { + const input = tags.stripIndent` + export { AppModule } from './app/app.module'; + `; + const output = tags.stripIndent` + export { AppModuleNgFactory } from "./app/app.module.ngfactory"; + export { AppModule } from './app/app.module'; + `; + + const transformer = exportNgFactory( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should export the ngfactory when there is a barrel file', () => { + const input = tags.stripIndent` + export { AppModule } from './app'; + `; + const output = tags.stripIndent` + export { AppModuleNgFactory } from "./app/app.module.ngfactory"; + export { AppModule } from './app'; + `; + + const transformer = exportNgFactory( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not do anything if shouldTransform returns false', () => { + const input = tags.stripIndent` + export { AppModule } from './app/app.module'; + `; + + const transformer = exportNgFactory( + () => false, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/index.ts b/packages/ngtools/webpack/src/transformers/index.ts new file mode 100644 index 0000000000..bb3d03dc92 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/index.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './interfaces'; +export * from './ast_helpers'; +export * from './make_transform'; +export * from './insert_import'; +export * from './elide_imports'; +export * from './replace_bootstrap'; +export * from './replace_server_bootstrap'; +export * from './export_ngfactory'; +export * from './export_lazy_module_map'; +export * from './register_locale_data'; +export * from './replace_resources'; +export * from './remove_decorators'; diff --git a/packages/ngtools/webpack/src/transformers/insert_import.ts b/packages/ngtools/webpack/src/transformers/insert_import.ts new file mode 100644 index 0000000000..e1583584f3 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/insert_import.ts @@ -0,0 +1,141 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { collectDeepNodes, getFirstNode } from './ast_helpers'; +import { AddNodeOperation, TransformOperation } from './interfaces'; + + +export function insertStarImport( + sourceFile: ts.SourceFile, + identifier: ts.Identifier, + modulePath: string, + target?: ts.Node, + before = false, +): TransformOperation[] { + const ops: TransformOperation[] = []; + const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration); + + // We don't need to verify if the symbol is already imported, star imports should be unique. + + // Create the new import node. + const namespaceImport = ts.createNamespaceImport(identifier); + const importClause = ts.createImportClause(undefined, namespaceImport); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(modulePath)); + + if (target) { + ops.push(new AddNodeOperation( + sourceFile, + target, + before ? newImport : undefined, + before ? undefined : newImport, + )); + } else if (allImports.length > 0) { + // Find the last import and insert after. + ops.push(new AddNodeOperation( + sourceFile, + allImports[allImports.length - 1], + undefined, + newImport, + )); + } else { + const firstNode = getFirstNode(sourceFile); + + if (firstNode) { + // Insert before the first node. + ops.push(new AddNodeOperation( + sourceFile, + firstNode, + newImport, + )); + } + } + + return ops; +} + + +export function insertImport( + sourceFile: ts.SourceFile, + symbolName: string, + modulePath: string, +): TransformOperation[] { + const ops: TransformOperation[] = []; + // Find all imports. + const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration); + const maybeImports = allImports + .filter((node: ts.ImportDeclaration) => { + // Filter all imports that do not match the modulePath. + return node.moduleSpecifier.kind == ts.SyntaxKind.StringLiteral + && (node.moduleSpecifier as ts.StringLiteral).text == modulePath; + }) + .filter((node: ts.ImportDeclaration) => { + // Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`. + const clause = node.importClause as ts.ImportClause; + if (!clause || clause.name || !clause.namedBindings) { + return false; + } + + return clause.namedBindings.kind == ts.SyntaxKind.NamedImports; + }) + .map((node: ts.ImportDeclaration) => { + // Return the `{ ... }` list of the named import. + return (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports; + }); + + if (maybeImports.length) { + // There's an `import {A, B, C} from 'modulePath'`. + // Find if it's in either imports. If so, just return; nothing to do. + const hasImportAlready = maybeImports.some((node: ts.NamedImports) => { + return node.elements.some((element: ts.ImportSpecifier) => { + return element.name.text == symbolName; + }); + }); + if (hasImportAlready) { + return ops; + } + + // Just pick the first one and insert at the end of its identifier list. + ops.push(new AddNodeOperation( + sourceFile, + maybeImports[0].elements[maybeImports[0].elements.length - 1], + undefined, + ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName)), + )); + } else { + // Create the new import node. + const namedImports = ts.createNamedImports([ts.createImportSpecifier(undefined, + ts.createIdentifier(symbolName))]); + const importClause = ts.createImportClause(undefined, namedImports); + const newImport = ts.createImportDeclaration(undefined, undefined, importClause, + ts.createLiteral(modulePath)); + + if (allImports.length > 0) { + // Find the last import and insert after. + ops.push(new AddNodeOperation( + sourceFile, + allImports[allImports.length - 1], + undefined, + newImport, + )); + } else { + const firstNode = getFirstNode(sourceFile); + + if (firstNode) { + // Insert before the first node. + ops.push(new AddNodeOperation( + sourceFile, + firstNode, + newImport, + )); + } + } + } + + return ops; +} diff --git a/packages/ngtools/webpack/src/transformers/interfaces.ts b/packages/ngtools/webpack/src/transformers/interfaces.ts new file mode 100644 index 0000000000..bc9b73002f --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/interfaces.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +export enum OPERATION_KIND { + Remove, + Add, + Replace, +} + +export interface StandardTransform { + (sourceFile: ts.SourceFile): TransformOperation[]; +} + +export abstract class TransformOperation { + constructor( + public kind: OPERATION_KIND, + public sourceFile: ts.SourceFile, + public target: ts.Node, + ) { } +} + +export class RemoveNodeOperation extends TransformOperation { + constructor(sourceFile: ts.SourceFile, target: ts.Node) { + super(OPERATION_KIND.Remove, sourceFile, target); + } +} + +export class AddNodeOperation extends TransformOperation { + constructor(sourceFile: ts.SourceFile, target: ts.Node, + public before?: ts.Node, public after?: ts.Node) { + super(OPERATION_KIND.Add, sourceFile, target); + } +} + +export class ReplaceNodeOperation extends TransformOperation { + kind: OPERATION_KIND.Replace; + constructor(sourceFile: ts.SourceFile, target: ts.Node, public replacement: ts.Node) { + super(OPERATION_KIND.Replace, sourceFile, target); + } +} diff --git a/packages/ngtools/webpack/src/transformers/make_transform.ts b/packages/ngtools/webpack/src/transformers/make_transform.ts new file mode 100644 index 0000000000..1b36d821a0 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/make_transform.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { elideImports } from './elide_imports'; +import { + AddNodeOperation, + OPERATION_KIND, + RemoveNodeOperation, + ReplaceNodeOperation, + StandardTransform, + TransformOperation, +} from './interfaces'; + + +// Typescript below 2.7.0 needs a workaround. +const tsVersionParts = ts.version.split('.').map(p => Number(p)); +const visitEachChild = tsVersionParts[0] <= 2 && tsVersionParts[1] < 7 + ? visitEachChildWorkaround + : ts.visitEachChild; + +export function makeTransform( + standardTransform: StandardTransform, + getTypeChecker?: () => ts.TypeChecker, +): ts.TransformerFactory { + + return (context: ts.TransformationContext): ts.Transformer => { + const transformer: ts.Transformer = (sf: ts.SourceFile) => { + const ops: TransformOperation[] = standardTransform(sf); + const removeOps = ops + .filter((op) => op.kind === OPERATION_KIND.Remove) as RemoveNodeOperation[]; + const addOps = ops.filter((op) => op.kind === OPERATION_KIND.Add) as AddNodeOperation[]; + const replaceOps = ops + .filter((op) => op.kind === OPERATION_KIND.Replace) as ReplaceNodeOperation[]; + + // If nodes are removed, elide the imports as well. + // Mainly a workaround for https://github.com/Microsoft/TypeScript/issues/17552. + // WARNING: this assumes that replaceOps DO NOT reuse any of the nodes they are replacing. + // This is currently true for transforms that use replaceOps (replace_bootstrap and + // replace_resources), but may not be true for new transforms. + if (getTypeChecker && removeOps.length + replaceOps.length > 0) { + const removedNodes = removeOps.concat(replaceOps).map((op) => op.target); + removeOps.push(...elideImports(sf, removedNodes, getTypeChecker)); + } + + const visitor: ts.Visitor = (node) => { + let modified = false; + let modifiedNodes = [node]; + // Check if node should be dropped. + if (removeOps.find((op) => op.target === node)) { + modifiedNodes = []; + modified = true; + } + + // Check if node should be replaced (only replaces with first op found). + const replace = replaceOps.find((op) => op.target === node); + if (replace) { + modifiedNodes = [replace.replacement]; + modified = true; + } + + // Check if node should be added to. + const add = addOps.filter((op) => op.target === node); + if (add.length > 0) { + modifiedNodes = [ + ...add.filter((op) => op.before).map(((op) => op.before)), + ...modifiedNodes, + ...add.filter((op) => op.after).map(((op) => op.after)), + ] as ts.Node[]; + modified = true; + } + + // If we changed anything, return modified nodes without visiting further. + if (modified) { + return modifiedNodes; + } else { + // Otherwise return node as is and visit children. + return visitEachChild(node, visitor, context); + } + }; + + // Don't visit the sourcefile at all if we don't have ops for it. + if (ops.length === 0) { + return sf; + } + + const result = ts.visitNode(sf, visitor); + + // If we removed any decorators, we need to clean up the decorator arrays. + if (removeOps.some((op) => op.target.kind === ts.SyntaxKind.Decorator)) { + cleanupDecorators(result); + } + + return result; + }; + + return transformer; + }; +} + +/** + * This is a version of `ts.visitEachChild` that works that calls our version + * of `updateSourceFileNode`, so that typescript doesn't lose type information + * for property decorators. + * See https://github.com/Microsoft/TypeScript/issues/17384 (fixed by + * https://github.com/Microsoft/TypeScript/pull/20314 and released in TS 2.7.0) and + * https://github.com/Microsoft/TypeScript/issues/17551 (fixed by + * https://github.com/Microsoft/TypeScript/pull/18051 and released on TS 2.5.0). + */ +function visitEachChildWorkaround( + node: ts.Node, + visitor: ts.Visitor, + context: ts.TransformationContext, +) { + + if (node.kind === ts.SyntaxKind.SourceFile) { + const sf = node as ts.SourceFile; + const statements = ts.visitLexicalEnvironment(sf.statements, visitor, context); + + if (statements === sf.statements) { + return sf; + } + // Note: Need to clone the original file (and not use `ts.updateSourceFileNode`) + // as otherwise TS fails when resolving types for decorators. + const sfClone = ts.getMutableClone(sf); + sfClone.statements = statements; + + return sfClone; + } + + return ts.visitEachChild(node, visitor, context); +} + + +// 1) If TS sees an empty decorator array, it will still emit a `__decorate` call. +// This seems to be a TS bug. +// 2) Also ensure nodes with modified decorators have parents +// built in TS transformers assume certain nodes have parents (fixed in TS 2.7+) +function cleanupDecorators(node: ts.Node) { + if (node.decorators) { + if (node.decorators.length == 0) { + node.decorators = undefined; + } else if (node.parent == undefined) { + const originalNode = ts.getParseTreeNode(node); + node.parent = originalNode.parent; + } + } + + ts.forEachChild(node, node => cleanupDecorators(node)); +} diff --git a/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts b/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts new file mode 100644 index 0000000000..580f3851e1 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/multiple_transformers_spec.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { createTypescriptContext, transformTypescript } from './ast_helpers'; +import { exportLazyModuleMap } from './export_lazy_module_map'; +import { exportNgFactory } from './export_ngfactory'; +import { removeDecorators } from './remove_decorators'; +import { replaceBootstrap } from './replace_bootstrap'; + + +describe('@ngtools/webpack transformers', () => { + describe('multiple_transformers', () => { + it('should apply multiple transformers on the same file', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + import { Component } from '@angular/core'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + class AppComponent { + title = 'app'; + } + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import * as __lazy_0__ from "app/lazy/lazy.module.ngfactory.ts"; + import * as __lazy_1__ from "app/lazy2/lazy2.module.ngfactory.ts"; + + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; + + class AppComponent { + constructor() { this.title = 'app'; } + } + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); + + export var LAZY_MODULE_MAP = { "./lazy/lazy.module#LazyModule": __lazy_0__.LazyModuleNgFactory, "./lazy2/lazy2.module#LazyModule2": __lazy_1__.LazyModule2NgFactory }; + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + + const shouldTransform = () => true; + const getEntryModule = () => + ({ path: '/project/src/app/app.module', className: 'AppModule' }); + const getTypeChecker = () => program.getTypeChecker(); + + + const transformers = [ + replaceBootstrap(shouldTransform, getEntryModule, getTypeChecker), + exportNgFactory(shouldTransform, getEntryModule), + exportLazyModuleMap(shouldTransform, + () => ({ + './lazy/lazy.module.ngfactory#LazyModuleNgFactory': + '/project/src/app/lazy/lazy.module.ngfactory.ts', + './lazy2/lazy2.module.ngfactory#LazyModule2NgFactory': + '/project/src/app/lazy2/lazy2.module.ngfactory.ts', + })), + removeDecorators(shouldTransform, getTypeChecker), + ]; + + const result = transformTypescript(undefined, transformers, program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/register_locale_data.ts b/packages/ngtools/webpack/src/transformers/register_locale_data.ts new file mode 100644 index 0000000000..fd2aa20566 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/register_locale_data.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { collectDeepNodes, getFirstNode } from './ast_helpers'; +import { insertStarImport } from './insert_import'; +import { AddNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + + +export function registerLocaleData( + shouldTransform: (fileName: string) => boolean, + getEntryModule: () => { path: string, className: string } | null, + locale: string, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + const entryModule = getEntryModule(); + + if (!shouldTransform(sourceFile.fileName) || !entryModule || !locale) { + return ops; + } + + // Find all identifiers using the entry module class name. + const entryModuleIdentifiers = collectDeepNodes(sourceFile, + ts.SyntaxKind.Identifier) + .filter(identifier => identifier.text === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + // Find the bootstrap call + entryModuleIdentifiers.forEach(entryModuleIdentifier => { + // Figure out if it's a `platformBrowserDynamic().bootstrapModule(AppModule)` call. + if (!( + entryModuleIdentifier.parent + && entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression + )) { + return; + } + + const callExpr = entryModuleIdentifier.parent as ts.CallExpression; + + if (callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { + return; + } + + const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression; + + if (propAccessExpr.name.text !== 'bootstrapModule' + || propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + const firstNode = getFirstNode(sourceFile); + + if (!firstNode) { + return; + } + + // Create the import node for the locale. + const localeNamespaceId = ts.createUniqueName('__NgCli_locale_'); + ops.push(...insertStarImport( + sourceFile, + localeNamespaceId, + `@angular/common/locales/${locale}`, + firstNode, + true, + )); + + // Create the import node for the registerLocaleData function. + const regIdentifier = ts.createIdentifier(`registerLocaleData`); + const regNamespaceId = ts.createUniqueName('__NgCli_locale_'); + ops.push( + ...insertStarImport(sourceFile, regNamespaceId, '@angular/common', firstNode, true), + ); + + // Create the register function call + const registerFunctionCall = ts.createCall( + ts.createPropertyAccess(regNamespaceId, regIdentifier), + undefined, + [ts.createPropertyAccess(localeNamespaceId, 'default')], + ); + const registerFunctionStatement = ts.createStatement(registerFunctionCall); + + ops.push(new AddNodeOperation( + sourceFile, + firstNode, + registerFunctionStatement, + )); + }); + + return ops; + }; + + return makeTransform(standardTransform); +} diff --git a/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts b/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts new file mode 100644 index 0000000000..1310bfce3e --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/register_locale_data_spec.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { transformTypescript } from './ast_helpers'; +import { registerLocaleData } from './register_locale_data'; + +describe('@ngtools/webpack transformers', () => { + describe('register_locale_data', () => { + it('should add locale imports', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + const output = tags.stripIndent` + import * as __NgCli_locale_1 from "@angular/common/locales/fr"; + import * as __NgCli_locale_2 from "@angular/common"; + __NgCli_locale_2.registerLocaleData(__NgCli_locale_1.default); + + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + const transformer = registerLocaleData( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + 'fr', + ); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not add locale imports when there is no entry module', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + const transformer = registerLocaleData(() => true, () => null, 'fr'); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/remove_decorators.ts b/packages/ngtools/webpack/src/transformers/remove_decorators.ts new file mode 100644 index 0000000000..4a0e1a8161 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/remove_decorators.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { collectDeepNodes } from './ast_helpers'; +import { RemoveNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + + +export function removeDecorators( + shouldTransform: (fileName: string) => boolean, + getTypeChecker: () => ts.TypeChecker, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + if (!shouldTransform(sourceFile.fileName)) { + return ops; + } + + collectDeepNodes(sourceFile, ts.SyntaxKind.Decorator) + .filter((decorator) => shouldRemove(decorator, getTypeChecker())) + .forEach((decorator) => { + // Remove the decorator node. + ops.push(new RemoveNodeOperation(sourceFile, decorator)); + }); + + return ops; + }; + + return makeTransform(standardTransform, getTypeChecker); +} + +function shouldRemove(decorator: ts.Decorator, typeChecker: ts.TypeChecker): boolean { + const origin = getDecoratorOrigin(decorator, typeChecker); + + return origin ? origin.module === '@angular/core' : false; +} + +// Decorator helpers. +interface DecoratorOrigin { + name: string; + module: string; +} + +function getDecoratorOrigin( + decorator: ts.Decorator, + typeChecker: ts.TypeChecker, +): DecoratorOrigin | null { + if (!ts.isCallExpression(decorator.expression)) { + return null; + } + + let identifier: ts.Node; + let name: string | undefined = undefined; + if (ts.isPropertyAccessExpression(decorator.expression.expression)) { + identifier = decorator.expression.expression.expression; + name = decorator.expression.expression.name.text; + } else if (ts.isIdentifier(decorator.expression.expression)) { + identifier = decorator.expression.expression; + } else { + return null; + } + + // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal + const symbol = typeChecker.getSymbolAtLocation(identifier); + if (symbol && symbol.declarations && symbol.declarations.length > 0) { + const declaration = symbol.declarations[0]; + let module: string | undefined = undefined; + if (ts.isImportSpecifier(declaration)) { + name = (declaration.propertyName || declaration.name).text; + module = declaration.parent + && declaration.parent.parent + && declaration.parent.parent.parent + && (declaration.parent.parent.parent.moduleSpecifier as ts.StringLiteral).text + || ''; + } else if (ts.isNamespaceImport(declaration)) { + // Use the name from the decorator namespace property access + module = declaration.parent + && declaration.parent.parent + && (declaration.parent.parent.moduleSpecifier as ts.StringLiteral).text; + } else if (ts.isImportClause(declaration)) { + name = declaration.name && declaration.name.text; + module = declaration.parent && (declaration.parent.moduleSpecifier as ts.StringLiteral).text; + } else { + return null; + } + + return { name: name || '', module: module || '' }; + } + + return null; +} diff --git a/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts b/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts new file mode 100644 index 0000000000..f8ead15735 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/remove_decorators_spec.ts @@ -0,0 +1,251 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { createTypescriptContext, transformTypescript } from './ast_helpers'; +import { removeDecorators } from './remove_decorators'; + +describe('@ngtools/webpack transformers', () => { + describe('decorator_remover', () => { + it('should remove Angular decorators', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + export class AppComponent { + constructor() { + this.title = 'app'; + } + } + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove non-Angular decorators', () => { + const input = tags.stripIndent` + import { Component } from 'another-lib'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = ` + import * as tslib_1 from "tslib"; + import { Component } from 'another-lib'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + ], AppComponent); + export { AppComponent }; + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should keep other decorators on class member', () => { + const input = tags.stripIndent` + import { Component, HostListener } from '@angular/core'; + import { AnotherDecorator } from 'another-lib'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + + @HostListener('document:keydown.escape') + @AnotherDecorator() + onEscape() { + console.log('run'); + } + } + `; + const output = tags.stripIndent` + import * as tslib_1 from "tslib"; + import { AnotherDecorator } from 'another-lib'; + + export class AppComponent { + constructor() { + this.title = 'app'; + } + + onEscape() { + console.log('run'); + } + } + tslib_1.__decorate([ + AnotherDecorator() + ], AppComponent.prototype, "onEscape", null); + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should keep other decorators on class declaration', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + import { AnotherDecorator } from 'another-lib'; + + @AnotherDecorator() + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + import * as tslib_1 from "tslib"; + import { AnotherDecorator } from 'another-lib'; + + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + AnotherDecorator() + ], AppComponent); + export { AppComponent }; + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should remove imports for identifiers within the decorator', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + import { ChangeDetectionStrategy } from '@angular/core'; + + @Component({ + selector: 'app-root', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + export class AppComponent { + constructor() { + this.title = 'app'; + } + } + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not remove imports from types that are still used', () => { + const input = tags.stripIndent` + import { Component, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + import { abc } from 'xyz'; + + @Component({ + selector: 'app-root', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + notify: EventEmitter = new EventEmitter(); + title = 'app'; + example = { abc }; + } + + export { ChangeDetectionStrategy }; + `; + const output = tags.stripIndent` + import { ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + import { abc } from 'xyz'; + + export class AppComponent { + constructor() { + this.notify = new EventEmitter(); + this.title = 'app'; + this.example = { abc }; + } + } + + export { ChangeDetectionStrategy }; + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = removeDecorators( + () => true, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts b/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts new file mode 100644 index 0000000000..fc05f86b92 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_bootstrap.ts @@ -0,0 +1,102 @@ +/** + * @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 { dirname, relative } from 'path'; +import * as ts from 'typescript'; +import { collectDeepNodes } from './ast_helpers'; +import { insertStarImport } from './insert_import'; +import { ReplaceNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + + +export function replaceBootstrap( + shouldTransform: (fileName: string) => boolean, + getEntryModule: () => { path: string, className: string } | null, + getTypeChecker: () => ts.TypeChecker, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + const entryModule = getEntryModule(); + + if (!shouldTransform(sourceFile.fileName) || !entryModule) { + return ops; + } + + // Find all identifiers. + const entryModuleIdentifiers = collectDeepNodes(sourceFile, + ts.SyntaxKind.Identifier) + .filter(identifier => identifier.text === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); + const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/'); + + // Find the bootstrap calls. + entryModuleIdentifiers.forEach(entryModuleIdentifier => { + // Figure out if it's a `platformBrowserDynamic().bootstrapModule(AppModule)` call. + if (!( + entryModuleIdentifier.parent + && entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression + )) { + return; + } + + const callExpr = entryModuleIdentifier.parent as ts.CallExpression; + + if (callExpr.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { + return; + } + + const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression; + + if (propAccessExpr.name.text !== 'bootstrapModule' + || propAccessExpr.expression.kind !== ts.SyntaxKind.CallExpression) { + return; + } + + const bootstrapModuleIdentifier = propAccessExpr.name; + const innerCallExpr = propAccessExpr.expression as ts.CallExpression; + + if (!( + innerCallExpr.expression.kind === ts.SyntaxKind.Identifier + && (innerCallExpr.expression as ts.Identifier).text === 'platformBrowserDynamic' + )) { + return; + } + + const platformBrowserDynamicIdentifier = innerCallExpr.expression as ts.Identifier; + + const idPlatformBrowser = ts.createUniqueName('__NgCli_bootstrap_'); + const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_'); + + // Add the transform operations. + const factoryClassName = entryModule.className + 'NgFactory'; + const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; + ops.push( + // Replace the entry module import. + ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), + new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, + ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))), + // Replace the platformBrowserDynamic import. + ...insertStarImport(sourceFile, idPlatformBrowser, '@angular/platform-browser'), + new ReplaceNodeOperation(sourceFile, platformBrowserDynamicIdentifier, + ts.createPropertyAccess(idPlatformBrowser, 'platformBrowser')), + new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, + ts.createIdentifier('bootstrapModuleFactory')), + ); + }); + + return ops; + }; + + return makeTransform(standardTransform, getTypeChecker); +} diff --git a/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts b/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts new file mode 100644 index 0000000000..c56e6d8f74 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_bootstrap_spec.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { createTypescriptContext, transformTypescript } from './ast_helpers'; +import { replaceBootstrap } from './replace_bootstrap'; + +describe('@ngtools/webpack transformers', () => { + describe('replace_bootstrap', () => { + it('should replace bootstrap', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should replace bootstrap when barrel files are used', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-browser"; + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.platformBrowser().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not replace bootstrap when there is no entry module', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformBrowserDynamic().bootstrapModule(AppModule); + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceBootstrap( + () => true, + () => null, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/replace_resources.ts b/packages/ngtools/webpack/src/transformers/replace_resources.ts new file mode 100644 index 0000000000..28ade37e6f --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_resources.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import { collectDeepNodes, getFirstNode } from './ast_helpers'; +import { + AddNodeOperation, + ReplaceNodeOperation, + StandardTransform, + TransformOperation, +} from './interfaces'; +import { makeTransform } from './make_transform'; + + +export function replaceResources( + shouldTransform: (fileName: string) => boolean, +): ts.TransformerFactory { + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + if (!shouldTransform(sourceFile.fileName)) { + return ops; + } + + const replacements = findResources(sourceFile); + + if (replacements.length > 0) { + + // Add the replacement operations. + ops.push(...(replacements.map((rep) => rep.replaceNodeOperation))); + + // If we added a require call, we need to also add typings for it. + // The typings need to be compatible with node typings, but also work by themselves. + + // interface NodeRequire {(id: string): any;} + const nodeRequireInterface = ts.createInterfaceDeclaration([], [], 'NodeRequire', [], [], [ + ts.createCallSignature([], [ + ts.createParameter([], [], undefined, 'id', undefined, + ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ]); + + // declare var require: NodeRequire; + const varRequire = ts.createVariableStatement( + [ts.createToken(ts.SyntaxKind.DeclareKeyword)], + [ts.createVariableDeclaration('require', ts.createTypeReferenceNode('NodeRequire', []))], + ); + + ops.push(new AddNodeOperation(sourceFile, getFirstNode(sourceFile), nodeRequireInterface)); + ops.push(new AddNodeOperation(sourceFile, getFirstNode(sourceFile), varRequire)); + } + + return ops; + }; + + return makeTransform(standardTransform); +} + +export interface ResourceReplacement { + resourcePaths: string[]; + replaceNodeOperation: ReplaceNodeOperation; +} + +export function findResources(sourceFile: ts.SourceFile): ResourceReplacement[] { + const replacements: ResourceReplacement[] = []; + + // Find all object literals. + collectDeepNodes(sourceFile, ts.SyntaxKind.ObjectLiteralExpression) + // Get all their property assignments. + .map(node => collectDeepNodes(node, ts.SyntaxKind.PropertyAssignment)) + // Flatten into a single array (from an array of array). + .reduce((prev, curr) => curr ? prev.concat(curr) : prev, []) + // We only want property assignments for the templateUrl/styleUrls keys. + .filter((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(node.name); + if (!key) { + // key is an expression, can't do anything. + return false; + } + + return key == 'templateUrl' || key == 'styleUrls'; + }) + // Replace templateUrl/styleUrls key with template/styles, and and paths with require('path'). + .forEach((node: ts.PropertyAssignment) => { + const key = _getContentOfKeyLiteral(node.name); + + if (key == 'templateUrl') { + const resourcePath = _getResourceRequest(node.initializer, sourceFile); + const requireCall = _createRequireCall(resourcePath); + const propAssign = ts.createPropertyAssignment('template', requireCall); + replacements.push({ + resourcePaths: [resourcePath], + replaceNodeOperation: new ReplaceNodeOperation(sourceFile, node, propAssign), + }); + } else if (key == 'styleUrls') { + const arr = collectDeepNodes(node, + ts.SyntaxKind.ArrayLiteralExpression); + if (!arr || arr.length == 0 || arr[0].elements.length == 0) { + return; + } + + const stylePaths = arr[0].elements.map((element: ts.Expression) => { + return _getResourceRequest(element, sourceFile); + }); + + const requireArray = ts.createArrayLiteral( + stylePaths.map((path) => _createRequireCall(path)), + ); + + const propAssign = ts.createPropertyAssignment('styles', requireArray); + replacements.push({ + resourcePaths: stylePaths, + replaceNodeOperation: new ReplaceNodeOperation(sourceFile, node, propAssign), + }); + } + }); + + return replacements; + +} + +function _getContentOfKeyLiteral(node?: ts.Node): string | null { + if (!node) { + return null; + } else if (node.kind == ts.SyntaxKind.Identifier) { + return (node as ts.Identifier).text; + } else if (node.kind == ts.SyntaxKind.StringLiteral) { + return (node as ts.StringLiteral).text; + } else { + return null; + } +} + +function _getResourceRequest(element: ts.Expression, sourceFile: ts.SourceFile) { + if ( + element.kind === ts.SyntaxKind.StringLiteral || + element.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral + ) { + const url = (element as ts.StringLiteral).text; + + // If the URL does not start with ./ or ../, prepends ./ to it. + return `${/^\.?\.\//.test(url) ? '' : './'}${url}`; + } else { + // if not string, just use expression directly + return element.getFullText(sourceFile); + } +} + +function _createRequireCall(path: string) { + return ts.createCall( + ts.createIdentifier('require'), + [], + [ts.createLiteral(path)], + ); +} diff --git a/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts new file mode 100644 index 0000000000..23e3a65acf --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_resources_spec.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { transformTypescript } from './ast_helpers'; +import { replaceResources } from './replace_resources'; + +describe('@ngtools/webpack transformers', () => { + describe('replace_resources', () => { + it('should replace resources', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css', './app.component.2.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = tags.stripIndent` + import * as tslib_1 from "tslib"; + import { Component } from '@angular/core'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + template: require("./app.component.html"), + styles: [require("./app.component.css"), require("./app.component.2.css")] + }) + ], AppComponent); + export { AppComponent }; + `; + + const transformer = replaceResources(() => true); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should replace resources with backticks', () => { + const input = ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: \`./app.component.html\`, + styleUrls: [\`./app.component.css\`, \`./app.component.2.css\`] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = ` + import * as tslib_1 from "tslib"; + import { Component } from '@angular/core'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + template: require("./app.component.html"), + styles: [require("./app.component.css"), require("./app.component.2.css")] + }) + ], AppComponent); + export { AppComponent }; + `; + + const transformer = replaceResources(() => true); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not replace resources if shouldTransform returns false', () => { + const input = tags.stripIndent` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css', './app.component.2.css'] + }) + export class AppComponent { + title = 'app'; + } + `; + const output = ` + import * as tslib_1 from "tslib"; + import { Component } from '@angular/core'; + let AppComponent = class AppComponent { + constructor() { + this.title = 'app'; + } + }; + AppComponent = tslib_1.__decorate([ + Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css', './app.component.2.css'] + }) + ], AppComponent); + export { AppComponent }; + `; + + const transformer = replaceResources(() => false); + const result = transformTypescript(input, [transformer]); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts new file mode 100644 index 0000000000..483080bf4a --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap.ts @@ -0,0 +1,140 @@ +/** + * @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 { dirname, relative } from 'path'; +import * as ts from 'typescript'; +import { collectDeepNodes } from './ast_helpers'; +import { insertStarImport } from './insert_import'; +import { ReplaceNodeOperation, StandardTransform, TransformOperation } from './interfaces'; +import { makeTransform } from './make_transform'; + +export function replaceServerBootstrap( + shouldTransform: (fileName: string) => boolean, + getEntryModule: () => { path: string, className: string } | null, + getTypeChecker: () => ts.TypeChecker, +): ts.TransformerFactory { + + const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) { + const ops: TransformOperation[] = []; + + const entryModule = getEntryModule(); + + if (!shouldTransform(sourceFile.fileName) || !entryModule) { + return ops; + } + + // Find all identifiers. + const entryModuleIdentifiers = collectDeepNodes(sourceFile, + ts.SyntaxKind.Identifier) + .filter(identifier => identifier.text === entryModule.className); + + if (entryModuleIdentifiers.length === 0) { + return []; + } + + const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path); + const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/'); + const factoryClassName = entryModule.className + 'NgFactory'; + const factoryModulePath = normalizedEntryModulePath + '.ngfactory'; + + // Find the bootstrap calls. + entryModuleIdentifiers.forEach(entryModuleIdentifier => { + if (!entryModuleIdentifier.parent) { + return; + } + + if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.CallExpression && + entryModuleIdentifier.parent.kind !== ts.SyntaxKind.PropertyAssignment) { + return; + } + + if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression) { + // Figure out if it's a `platformDynamicServer().bootstrapModule(AppModule)` call. + + const callExpr = entryModuleIdentifier.parent as ts.CallExpression; + + if (callExpr.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { + + const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression; + + if (!(propAccessExpr.name.text === 'bootstrapModule' + && propAccessExpr.expression.kind === ts.SyntaxKind.CallExpression)) { + return; + } + + const bootstrapModuleIdentifier = propAccessExpr.name; + const innerCallExpr = propAccessExpr.expression as ts.CallExpression; + + if (!( + innerCallExpr.expression.kind === ts.SyntaxKind.Identifier + && (innerCallExpr.expression as ts.Identifier).text === 'platformDynamicServer' + )) { + return; + } + + const platformDynamicServerIdentifier = innerCallExpr.expression as ts.Identifier; + + const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_'); + const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_'); + + // Add the transform operations. + ops.push( + // Replace the entry module import. + ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), + new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, + ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))), + // Replace the platformBrowserDynamic import. + ...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'), + new ReplaceNodeOperation(sourceFile, platformDynamicServerIdentifier, + ts.createPropertyAccess(idPlatformServer, 'platformServer')), + new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier, + ts.createIdentifier('bootstrapModuleFactory')), + ); + } else if (callExpr.expression.kind === ts.SyntaxKind.Identifier) { + // Figure out if it is renderModule + + const identifierExpr = callExpr.expression as ts.Identifier; + + if (identifierExpr.text !== 'renderModule') { + return; + } + + const renderModuleIdentifier = identifierExpr as ts.Identifier; + + const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_'); + const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_'); + + ops.push( + // Replace the entry module import. + ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), + new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, + ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))), + // Replace the renderModule import. + ...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'), + new ReplaceNodeOperation(sourceFile, renderModuleIdentifier, + ts.createPropertyAccess(idPlatformServer, 'renderModuleFactory')), + ); + } + } else if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.PropertyAssignment) { + // This is for things that accept a module as a property in a config object + // .ie the express engine + + const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_'); + + ops.push( + ...insertStarImport(sourceFile, idNgFactory, factoryModulePath), + new ReplaceNodeOperation(sourceFile, entryModuleIdentifier, + ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))), + ); + } + }); + + return ops; + }; + + return makeTransform(standardTransform, getTypeChecker); +} diff --git a/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts new file mode 100644 index 0000000000..1d7ebcd0b8 --- /dev/null +++ b/packages/ngtools/webpack/src/transformers/replace_server_bootstrap_spec.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; // tslint:disable-line:no-implicit-dependencies +import { createTypescriptContext, transformTypescript } from './ast_helpers'; +import { replaceServerBootstrap } from './replace_server_bootstrap'; + +describe('@ngtools/webpack transformers', () => { + describe('replace_server_bootstrap', () => { + it('should replace bootstrap', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformDynamicServer } from '@angular/platform-server'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformDynamicServer().bootstrapModule(AppModule); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-server"; + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceServerBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should replace renderModule', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { renderModule } from '@angular/platform-server'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + renderModule(AppModule, { + document: '', + url: '/' + }); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-server"; + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.renderModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory, { + document: '', + url: '/' + }); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceServerBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should replace when the module is used in a config object', () => { + const input = tags.stripIndent` + import * as express from 'express'; + + import { enableProdMode } from '@angular/core'; + import { ngExpressEngine } from '@nguniversal/express-engine'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + const server = express(); + server.engine('html', ngExpressEngine({ + bootstrap: AppModule + })); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import * as express from 'express'; + + import { enableProdMode } from '@angular/core'; + import { ngExpressEngine } from '@nguniversal/express-engine'; + + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + + if (environment.production) { + enableProdMode(); + } + + const server = express(); + server.engine('html', ngExpressEngine({ + bootstrap: __NgCli_bootstrap_1.AppModuleNgFactory + })); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceServerBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should replace bootstrap when barrel files are used', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformDynamicServer } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformDynamicServer().bootstrapModule(AppModule); + `; + + // tslint:disable:max-line-length + const output = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { environment } from './environments/environment'; + + import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory"; + import * as __NgCli_bootstrap_2 from "@angular/platform-server"; + + if (environment.production) { + enableProdMode(); + } + __NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory); + `; + // tslint:enable:max-line-length + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceServerBootstrap( + () => true, + () => ({ path: '/project/src/app/app.module', className: 'AppModule' }), + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${output}`); + }); + + it('should not replace bootstrap when there is no entry module', () => { + const input = tags.stripIndent` + import { enableProdMode } from '@angular/core'; + import { platformDynamicServer } from '@angular/platform-browser-dynamic'; + + import { AppModule } from './app/app.module'; + import { environment } from './environments/environment'; + + if (environment.production) { + enableProdMode(); + } + + platformDynamicServer().bootstrapModule(AppModule); + `; + + const { program, compilerHost } = createTypescriptContext(input); + const transformer = replaceServerBootstrap( + () => true, + () => null, + () => program.getTypeChecker(), + ); + const result = transformTypescript(undefined, [transformer], program, compilerHost); + + expect(tags.oneLine`${result}`).toEqual(tags.oneLine`${input}`); + }); + }); +}); diff --git a/packages/ngtools/webpack/src/type_checker.ts b/packages/ngtools/webpack/src/type_checker.ts new file mode 100644 index 0000000000..0eeff80ccb --- /dev/null +++ b/packages/ngtools/webpack/src/type_checker.ts @@ -0,0 +1,142 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { terminal } from '@angular-devkit/core'; +import * as ts from 'typescript'; +import { time, timeEnd } from './benchmark'; +import { WebpackCompilerHost } from './compiler_host'; +import { CancellationToken, gatherDiagnostics } from './gather_diagnostics'; +import { + CompilerHost, + CompilerOptions, + Program, + createCompilerHost, + createProgram, + formatDiagnostics, +} from './ngtools_api'; + + +// This file should run in a child process with the AUTO_START_ARG argument + + +export enum MESSAGE_KIND { + Init, + Update, +} + +export abstract class TypeCheckerMessage { + constructor(public kind: MESSAGE_KIND) { } +} + +export class InitMessage extends TypeCheckerMessage { + constructor( + public compilerOptions: ts.CompilerOptions, + public basePath: string, + public jitMode: boolean, + public rootNames: string[], + ) { + super(MESSAGE_KIND.Init); + } +} + +export class UpdateMessage extends TypeCheckerMessage { + constructor(public rootNames: string[], public changedCompilationFiles: string[]) { + super(MESSAGE_KIND.Update); + } +} + +export const AUTO_START_ARG = '9d93e901-158a-4cf9-ba1b-2f0582ffcfeb'; + +export class TypeChecker { + private _program: ts.Program | Program; + private _compilerHost: WebpackCompilerHost & CompilerHost; + + constructor( + private _compilerOptions: CompilerOptions, + _basePath: string, + private _JitMode: boolean, + private _rootNames: string[], + ) { + time('TypeChecker.constructor'); + const compilerHost = new WebpackCompilerHost(_compilerOptions, _basePath); + compilerHost.enableCaching(); + // We don't set a async resource loader on the compiler host because we only support + // html templates, which are the only ones that can throw errors, and those can be loaded + // synchronously. + // If we need to also report errors on styles then we'll need to ask the main thread + // for these resources. + this._compilerHost = createCompilerHost({ + options: this._compilerOptions, + tsHost: compilerHost, + }) as CompilerHost & WebpackCompilerHost; + timeEnd('TypeChecker.constructor'); + } + + private _update(rootNames: string[], changedCompilationFiles: string[]) { + time('TypeChecker._update'); + this._rootNames = rootNames; + changedCompilationFiles.forEach((fileName) => { + this._compilerHost.invalidate(fileName); + }); + timeEnd('TypeChecker._update'); + } + + private _createOrUpdateProgram() { + if (this._JitMode) { + // Create the TypeScript program. + time('TypeChecker._createOrUpdateProgram.ts.createProgram'); + this._program = ts.createProgram( + this._rootNames, + this._compilerOptions, + this._compilerHost, + this._program as ts.Program, + ) as ts.Program; + timeEnd('TypeChecker._createOrUpdateProgram.ts.createProgram'); + } else { + time('TypeChecker._createOrUpdateProgram.ng.createProgram'); + // Create the Angular program. + this._program = createProgram({ + rootNames: this._rootNames, + options: this._compilerOptions, + host: this._compilerHost, + oldProgram: this._program as Program, + }) as Program; + timeEnd('TypeChecker._createOrUpdateProgram.ng.createProgram'); + } + } + + private _diagnose(cancellationToken: CancellationToken) { + const allDiagnostics = gatherDiagnostics( + this._program, this._JitMode, 'TypeChecker', cancellationToken); + + // Report diagnostics. + if (!cancellationToken.isCancellationRequested()) { + const errors = allDiagnostics.filter((d) => d.category === ts.DiagnosticCategory.Error); + const warnings = allDiagnostics.filter((d) => d.category === ts.DiagnosticCategory.Warning); + + if (errors.length > 0) { + const message = formatDiagnostics(errors); + console.error(terminal.bold(terminal.red('ERROR in ' + message))); + } else { + // Reset the changed file tracker only if there are no errors. + this._compilerHost.resetChangedFileTracker(); + } + + if (warnings.length > 0) { + const message = formatDiagnostics(warnings); + console.log(terminal.bold(terminal.yellow('WARNING in ' + message))); + } + } + } + + public update(rootNames: string[], changedCompilationFiles: string[], + cancellationToken: CancellationToken) { + this._update(rootNames, changedCompilationFiles); + this._createOrUpdateProgram(); + this._diagnose(cancellationToken); + } +} diff --git a/packages/ngtools/webpack/src/type_checker_bootstrap.js b/packages/ngtools/webpack/src/type_checker_bootstrap.js new file mode 100644 index 0000000000..9326a42d9b --- /dev/null +++ b/packages/ngtools/webpack/src/type_checker_bootstrap.js @@ -0,0 +1,2 @@ +require('../../../../lib/bootstrap-local'); +require('./type_checker_worker.ts'); diff --git a/packages/ngtools/webpack/src/type_checker_worker.ts b/packages/ngtools/webpack/src/type_checker_worker.ts new file mode 100644 index 0000000000..7774772923 --- /dev/null +++ b/packages/ngtools/webpack/src/type_checker_worker.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as process from 'process'; +import { time, timeEnd } from './benchmark'; +import { CancellationToken } from './gather_diagnostics'; +import { + AUTO_START_ARG, + InitMessage, + MESSAGE_KIND, + TypeChecker, + TypeCheckerMessage, + UpdateMessage, +} from './type_checker'; + +let typeChecker: TypeChecker; +let lastCancellationToken: CancellationToken; + +// only listen to messages if started from the AngularCompilerPlugin +if (process.argv.indexOf(AUTO_START_ARG) >= 0) { + process.on('message', (message: TypeCheckerMessage) => { + time('TypeChecker.message'); + switch (message.kind) { + case MESSAGE_KIND.Init: + const initMessage = message as InitMessage; + typeChecker = new TypeChecker( + initMessage.compilerOptions, + initMessage.basePath, + initMessage.jitMode, + initMessage.rootNames, + ); + break; + case MESSAGE_KIND.Update: + if (!typeChecker) { + throw new Error('TypeChecker: update message received before initialization'); + } + if (lastCancellationToken) { + // This cancellation token doesn't seem to do much, messages don't seem to be processed + // before the diagnostics finish. + lastCancellationToken.requestCancellation(); + } + const updateMessage = message as UpdateMessage; + lastCancellationToken = new CancellationToken(); + typeChecker.update(updateMessage.rootNames, updateMessage.changedCompilationFiles, + lastCancellationToken); + break; + default: + throw new Error(`TypeChecker: Unexpected message received: ${message}.`); + } + timeEnd('TypeChecker.message'); + }); +} diff --git a/packages/ngtools/webpack/src/virtual_file_system_decorator.ts b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts new file mode 100644 index 0000000000..e7b4cb049f --- /dev/null +++ b/packages/ngtools/webpack/src/virtual_file_system_decorator.ts @@ -0,0 +1,143 @@ +/** + * @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 { Stats } from 'fs'; +import { WebpackCompilerHost } from './compiler_host'; +import { Callback, InputFileSystem, NodeWatchFileSystemInterface } from './webpack'; + +export const NodeWatchFileSystem: NodeWatchFileSystemInterface = require( + 'webpack/lib/node/NodeWatchFileSystem'); + +export class VirtualFileSystemDecorator implements InputFileSystem { + constructor( + private _inputFileSystem: InputFileSystem, + private _webpackCompilerHost: WebpackCompilerHost, + ) { } + + // We only need to intercept calls to individual files that are present in WebpackCompilerHost. + private _readFileSync(path: string): string | null { + if (this._webpackCompilerHost.fileExists(path, false)) { + return this._webpackCompilerHost.readFile(path) || null; + } + + return null; + } + + private _statSync(path: string): Stats | null { + if (this._webpackCompilerHost.fileExists(path, false)) { + return this._webpackCompilerHost.stat(path); + } + + return null; + } + + getVirtualFilesPaths() { + return this._webpackCompilerHost.getNgFactoryPaths(); + } + + stat(path: string, callback: Callback): void { + const result = this._statSync(path); + if (result) { + callback(null, result); + } else { + this._inputFileSystem.stat(path, callback); + } + } + + readdir(path: string, callback: Callback): void { + this._inputFileSystem.readdir(path, callback); + } + + readFile(path: string, callback: Callback): void { + const result = this._readFileSync(path); + if (result) { + callback(null, result); + } else { + this._inputFileSystem.readFile(path, callback); + } + } + + readJson(path: string, callback: Callback<{}>): void { + this._inputFileSystem.readJson(path, callback); + } + + readlink(path: string, callback: Callback): void { + this._inputFileSystem.readlink(path, callback); + } + + statSync(path: string): Stats { + const result = this._statSync(path); + + return result || this._inputFileSystem.statSync(path); + } + + readdirSync(path: string): string[] { + return this._inputFileSystem.readdirSync(path); + } + + readFileSync(path: string): string { + const result = this._readFileSync(path); + + return result || this._inputFileSystem.readFileSync(path); + } + + readJsonSync(path: string): string { + return this._inputFileSystem.readJsonSync(path); + } + + readlinkSync(path: string): string { + return this._inputFileSystem.readlinkSync(path); + } + + purge(changes?: string[] | string): void { + if (typeof changes === 'string') { + this._webpackCompilerHost.invalidate(changes); + } else if (Array.isArray(changes)) { + changes.forEach((fileName: string) => this._webpackCompilerHost.invalidate(fileName)); + } + if (this._inputFileSystem.purge) { + this._inputFileSystem.purge(changes); + } + } +} + +export class VirtualWatchFileSystemDecorator extends NodeWatchFileSystem { + constructor(private _virtualInputFileSystem: VirtualFileSystemDecorator) { + super(_virtualInputFileSystem); + } + + watch( + files: any, // tslint:disable-line:no-any + dirs: any, // tslint:disable-line:no-any + missing: any, // tslint:disable-line:no-any + startTime: any, // tslint:disable-line:no-any + options: any, // tslint:disable-line:no-any + callback: any, // tslint:disable-line:no-any + callbackUndelayed: any, // tslint:disable-line:no-any + ) { + const newCallback = ( + err: any, // tslint:disable-line:no-any + filesModified: any, // tslint:disable-line:no-any + contextModified: any, // tslint:disable-line:no-any + missingModified: any, // tslint:disable-line:no-any + fileTimestamps: { [k: string]: number }, + contextTimestamps: { [k: string]: number }, + ) => { + // Update fileTimestamps with timestamps from virtual files. + const virtualFilesStats = this._virtualInputFileSystem.getVirtualFilesPaths() + .map((fileName) => ({ + path: fileName, + mtime: +this._virtualInputFileSystem.statSync(fileName).mtime, + })); + virtualFilesStats.forEach(stats => fileTimestamps[stats.path] = +stats.mtime); + callback(err, filesModified, contextModified, missingModified, fileTimestamps, + contextTimestamps); + }; + + return super.watch(files, dirs, missing, startTime, options, newCallback, callbackUndelayed); + } +} diff --git a/packages/ngtools/webpack/src/webpack.ts b/packages/ngtools/webpack/src/webpack.ts new file mode 100644 index 0000000000..660f39edf5 --- /dev/null +++ b/packages/ngtools/webpack/src/webpack.ts @@ -0,0 +1,74 @@ +/** + * @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 { Stats } from 'fs'; + +// Declarations for (some) Webpack types. Only what's needed. + +export interface Request { + request?: Request; + relativePath: string; +} + +export interface Callback { + (err?: Error | null, result?: T): void; +} + +export interface ResolverCallback { + (request: Request, callback: Callback): void; +} + +export interface Tapable { + apply(plugin: ResolverPlugin): void; +} + +export interface ResolverPlugin extends Tapable { + plugin(source: string, cb: ResolverCallback): void; + // tslint:disable-next-line:no-any + doResolve(target: string, req: Request, desc: string, callback: Callback): void; + join(relativePath: string, innerRequest: Request): Request; +} + +export interface LoaderCallback { + (err: Error | null, source?: string, sourceMap?: string): void; +} + +export interface NormalModuleFactory { + plugin(event: string, + // tslint:disable-next-line:no-any + callback: (data: NormalModuleFactoryRequest, callback: Callback) => void): any; +} + +export interface NormalModuleFactoryRequest { + request: string; + contextInfo: { issuer: string }; +} + +export interface InputFileSystem { + stat(path: string, callback: Callback): void; + readdir(path: string, callback: Callback): void; + readFile(path: string, callback: Callback): void; + // tslint:disable-next-line:no-any + readJson(path: string, callback: Callback): void; + readlink(path: string, callback: Callback): void; + statSync(path: string): Stats; + readdirSync(path: string): string[]; + readFileSync(path: string): string; + // tslint:disable-next-line:no-any + readJsonSync(path: string): any; + readlinkSync(path: string): string; + purge(changes?: string[] | string): void; +} + +export interface NodeWatchFileSystemInterface { + inputFileSystem: InputFileSystem; + new(inputFileSystem: InputFileSystem): NodeWatchFileSystemInterface; + // tslint:disable-next-line:no-any + watch(files: any, dirs: any, missing: any, startTime: any, options: any, callback: any, + // tslint:disable-next-line:no-any + callbackUndelayed: any): any; +} diff --git a/packages/ngtools/webpack/tsconfig.json b/packages/ngtools/webpack/tsconfig.json new file mode 100644 index 0000000000..46fb29b75b --- /dev/null +++ b/packages/ngtools/webpack/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.json", + + "compilerOptions": { + "outDir": "../../../dist/@ngtools/webpack", + "rootDir": ".", + "baseUrl": "" + } +} diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 11e61605e0..2513042a06 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize } from '@angular-devkit/core'; +import { JsonObject, normalize } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -15,6 +15,12 @@ import { schematic, } from '@angular-devkit/schematics'; import * as ts from 'typescript'; +import { + Architect, + Project, + WorkspaceSchema, +} from '../../../angular_devkit/core/src/workspace/workspace-schema'; +import { Schema as ComponentOptions } from '../component/schema'; import { addImportToModule, addSymbolToNgModuleMetadata, @@ -24,7 +30,7 @@ import { isImported, } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; -import { AppConfig, getAppFromConfig, getConfig } from '../utility/config'; +import { getWorkspace, getWorkspacePath } from '../utility/config'; import { getAppModulePath } from '../utility/ng-ast-utils'; import { insertImport } from '../utility/route-utils'; import { Schema as AppShellOptions } from './schema'; @@ -40,7 +46,7 @@ function formatMissingAppMsg(label: string, nameOrIndex: string | undefined): st function getSourceFile(host: Tree, path: string): ts.SourceFile { const buffer = host.read(path); if (!buffer) { - throw new SchematicsException(`Could not find bootstrapped module.`); + throw new SchematicsException(`Could not find ${path}.`); } const content = buffer.toString(); const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); @@ -48,8 +54,8 @@ function getSourceFile(host: Tree, path: string): ts.SourceFile { return source; } -function getServerModulePath(host: Tree, app: AppConfig): string | null { - const mainPath = `/${app.root}/${app.main}`; +function getServerModulePath(host: Tree, project: Project, architect: Architect): string | null { + const mainPath = architect.server.options.main; const mainSource = getSourceFile(host, mainPath); const allNodes = getSourceNodes(mainSource); const expNode = allNodes.filter(node => node.kind === ts.SyntaxKind.ExportDeclaration)[0]; @@ -57,7 +63,7 @@ function getServerModulePath(host: Tree, app: AppConfig): string | null { return null; } const relativePath = ( expNode).moduleSpecifier; - const modulePath = normalize(`/${app.root}/${relativePath.text}.ts`); + const modulePath = normalize(`/${project.root}/src/${relativePath.text}.ts`); return modulePath; } @@ -96,8 +102,12 @@ function getComponentTemplate(host: Tree, compPath: string, tmplInfo: TemplateIn return template; } -function getBootstrapComponentPath(host: Tree, appConfig: AppConfig): string { - const modulePath = getAppModulePath(host, appConfig); +function getBootstrapComponentPath(host: Tree, project: Project): string { + if (!project.architect) { + throw new Error('Project architect not found.'); + } + const mainPath = project.architect.build.options.main; + const modulePath = getAppModulePath(host, mainPath); const moduleSource = getSourceFile(host, modulePath); const metadataNode = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core')[0]; @@ -131,13 +141,8 @@ function validateProject(options: AppShellOptions): Rule { return (host: Tree, context: SchematicContext) => { const routerOutletCheckRegex = /([\s\S]*?)<\/router\-outlet>/; - const config = getConfig(host); - const app = getAppFromConfig(config, options.clientApp || '0'); - if (app === null) { - throw new SchematicsException(formatMissingAppMsg('Client', options.clientApp)); - } - - const componentPath = getBootstrapComponentPath(host, app); + const clientProject = getClientProject(host, options); + const componentPath = getBootstrapComponentPath(host, clientProject); const tmpl = getComponentTemplateInfo(host, componentPath); const template = getComponentTemplate(host, componentPath, tmpl); if (!routerOutletCheckRegex.test(template)) { @@ -149,51 +154,54 @@ function validateProject(options: AppShellOptions): Rule { }; } -function addUniversalApp(options: AppShellOptions): Rule { +function addUniversalTarget(options: AppShellOptions): Rule { return (host: Tree, context: SchematicContext) => { - const config = getConfig(host); - const appConfig = getAppFromConfig(config, options.universalApp); - - if (appConfig && appConfig.platform === 'server') { - return host; - } else if (appConfig && appConfig.platform !== 'server') { - throw new SchematicsException( - `Invalid platform for universal app (${options.universalApp}), value must be "server".`); + const architect = getClientArchitect(host, options); + if (architect !== null) { + if (architect.server !== undefined) { + return host; + } } // Copy options. const universalOptions = { ...options, - name: options.universalApp, + name: options.universalProject, }; // Delete non-universal options. - delete universalOptions.universalApp; + delete universalOptions.universalProject; delete universalOptions.route; return schematic('universal', universalOptions)(host, context); }; } -function addAppShellConfig(options: AppShellOptions): Rule { +function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const app = getAppFromConfig(config, options.clientApp || '0'); - - if (!app) { - throw new SchematicsException(formatMissingAppMsg('Client', options.clientApp)); - } - if (!options.route) { throw new SchematicsException(`Route is not defined`); } - app.appShell = { - app: options.universalApp, + const workspace = getWorkspace(host); + const workspacePath = getWorkspacePath(host); + + const appShellTarget: JsonObject = { + browserTarget: `${options.clientProject}:build`, + serverTarget: `${options.clientProject}:server`, route: options.route, }; - host.overwrite('/.angular-cli.json', JSON.stringify(config, null, 2)); + if (!workspace.projects[options.clientProject]) { + throw new SchematicsException(`Client app ${options.clientProject} not found.`); + } + const clientProject = workspace.projects[options.clientProject]; + if (!clientProject.architect) { + throw new Error('Client project architect not found.'); + } + clientProject.architect['app-shell'] = appShellTarget; + + host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); return host; }; @@ -201,12 +209,9 @@ function addAppShellConfig(options: AppShellOptions): Rule { function addRouterModule(options: AppShellOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const app = getAppFromConfig(config, options.clientApp || '0'); - if (app === null) { - throw new SchematicsException(formatMissingAppMsg('Client', options.clientApp)); - } - const modulePath = getAppModulePath(host, app); + const clientArchitect = getClientArchitect(host, options); + const mainPath = clientArchitect.build.options.main; + const modulePath = getAppModulePath(host, mainPath); const moduleSource = getSourceFile(host, modulePath); const changes = addImportToModule(moduleSource, modulePath, 'RouterModule', '@angular/router'); const recorder = host.beginUpdate(modulePath); @@ -240,12 +245,10 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper function addServerRoutes(options: AppShellOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const app = getAppFromConfig(config, options.universalApp); - if (app === null) { - throw new SchematicsException(formatMissingAppMsg('Universal/server', options.universalApp)); - } - const modulePath = getServerModulePath(host, app); + const clientProject = getClientProject(host, options); + const architect = getClientArchitect(host, options); + // const mainPath = universalArchitect.build.options.main; + const modulePath = getServerModulePath(host, clientProject, architect); if (modulePath === null) { throw new SchematicsException('Universal/server module not found.'); } @@ -301,20 +304,41 @@ function addServerRoutes(options: AppShellOptions): Rule { function addShellComponent(options: AppShellOptions): Rule { return (host: Tree, context: SchematicContext) => { - const componentOptions = { + const componentOptions: ComponentOptions = { name: 'app-shell', module: options.rootModuleFileName, + project: options.clientProject, }; return schematic('component', componentOptions)(host, context); }; } +function getClientProject(host: Tree, options: AppShellOptions): Project { + const workspace = getWorkspace(host); + const clientProject = workspace.projects[options.clientProject]; + if (!clientProject) { + throw new SchematicsException(formatMissingAppMsg('Client', options.clientProject)); + } + + return clientProject; +} + +function getClientArchitect(host: Tree, options: AppShellOptions): Architect { + const clientArchitect = getClientProject(host, options).architect; + + if (!clientArchitect) { + throw new Error('Client project architect not found.'); + } + + return clientArchitect; +} + export default function (options: AppShellOptions): Rule { return chain([ validateProject(options), - addUniversalApp(options), - addAppShellConfig(options), + addUniversalTarget(options), + addAppShellConfigToWorkspace(options), addRouterModule(options), addServerRoutes(options), addShellComponent(options), diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index 0cb4820b47..23c72d1749 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -5,10 +5,10 @@ * Use 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 { Tree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as AppShellOptions } from './schema'; @@ -19,30 +19,36 @@ describe('App Shell Schematic', () => { ); const defaultOptions: AppShellOptions = { name: 'foo', - universalApp: 'universal', + clientProject: 'bar', + universalProject: 'universal', + }; + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', }; - let appTree: Tree; const appOptions: ApplicationOptions = { - directory: '', - name: 'appshell-app', - sourceDir: 'src', + name: 'bar', inlineStyle: false, inlineTemplate: false, - viewEncapsulation: 'None', - version: '1.2.3', routing: true, style: 'css', skipTests: false, - minimal: false, + skipPackageJson: false, }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = schematicRunner.runSchematic('application', appOptions); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); + it('should ensure the client app has a router-outlet', () => { - appTree = schematicRunner.runSchematic('application', {...appOptions, routing: false}); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', {...appOptions, routing: false}, appTree); expect(() => { schematicRunner.runSchematic('appShell', defaultOptions, appTree); }).toThrowError(); @@ -50,48 +56,30 @@ describe('App Shell Schematic', () => { it('should add a universal app', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/src/app/app.server.module.ts'; - const file = tree.files.filter(f => f === filePath)[0]; - expect(file).toBeDefined(); - }); - - it('should use an existing universal app', () => { - const universalOptions = { - name: defaultOptions.universalApp, - }; - - let tree = schematicRunner.runSchematic('universal', universalOptions, appTree); - const filePath = '/.angular-cli.json'; - let content = tree.readContent(filePath); - let config = JSON.parse(content); - const appCount = config.apps.length; - - tree = schematicRunner.runSchematic('appShell', defaultOptions, tree); - content = tree.readContent(filePath); - config = JSON.parse(content); - expect(config.apps.length).toBe(appCount); + const filePath = '/projects/bar/src/app/app.server.module.ts'; + expect(tree.exists(filePath)).toEqual(true); }); it('should add app shell configuration', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/.angular-cli.json'; + const filePath = '/angular.json'; const content = tree.readContent(filePath); - const config = JSON.parse(content); - const app = config.apps[0]; - expect(app.appShell).toBeDefined(); - expect(app.appShell.app).toEqual('universal'); - expect(app.appShell.route).toEqual('shell'); + const workspace = JSON.parse(content); + const target = workspace.projects.bar.architect['app-shell']; + expect(target.browserTarget).toEqual('bar:build'); + expect(target.serverTarget).toEqual('bar:server'); + expect(target.route).toEqual('shell'); }); it('should add router module to client app module', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/src/app/app.module.ts'; + const filePath = '/projects/bar/src/app/app.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/import { RouterModule } from \'@angular\/router\';/); }); describe('Add router-outlet', () => { - function makeInlineTemplate(tree: Tree, template?: string): void { + function makeInlineTemplate(tree: UnitTestTree, template?: string): void { template = template || `

App works! @@ -116,12 +104,12 @@ describe('App Shell Schematic', () => { } `; - tree.overwrite('/src/app/app.component.ts', newText); - tree.delete('/src/app/app.component.html'); + tree.overwrite('/projects/bar/src/app/app.component.ts', newText); + tree.delete('/projects/bar/src/app/app.component.html'); } it('should not re-add the router outlet (external template)', () => { - const htmlPath = '/src/app/app.component.html'; + const htmlPath = '/projects/bar/src/app/app.component.html'; appTree.overwrite(htmlPath, ''); const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); @@ -134,7 +122,7 @@ describe('App Shell Schematic', () => { it('should not re-add the router outlet (inline template)', () => { makeInlineTemplate(appTree, ''); const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const content = tree.readContent('/src/app/app.component.ts'); + const content = tree.readContent('/projects/bar/src/app/app.component.ts'); const matches = content.match(/<\/router\-outlet>/g); const numMatches = matches ? matches.length : 0; expect(numMatches).toEqual(1); @@ -143,22 +131,21 @@ describe('App Shell Schematic', () => { it('should add router imports to server module', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/src/app/app.server.module.ts'; + const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); - // tslint:disable-next-line:max-line-length expect(content).toMatch(/import { Routes, RouterModule } from \'@angular\/router\';/); }); it('should define a server route', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/src/app/app.server.module.ts'; + const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/const routes: Routes = \[/); }); it('should import RouterModule with forRoot', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - const filePath = '/src/app/app.server.module.ts'; + const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content) .toMatch(/const routes: Routes = \[ { path: 'shell', component: AppShellComponent }\];/); @@ -168,8 +155,8 @@ describe('App Shell Schematic', () => { it('should create the shell component', () => { const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); - expect(tree.exists('/src/app/app-shell/app-shell.component.ts')); - const content = tree.readContent('/src/app/app.server.module.ts'); + expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')); + const content = tree.readContent('/projects/bar/src/app/app.server.module.ts'); expect(content).toMatch(/app\-shell\.component/); }); }); diff --git a/packages/schematics/angular/app-shell/schema.d.ts b/packages/schematics/angular/app-shell/schema.d.ts index c814cd9379..4946258e9b 100644 --- a/packages/schematics/angular/app-shell/schema.d.ts +++ b/packages/schematics/angular/app-shell/schema.d.ts @@ -8,13 +8,13 @@ export interface Schema { /** - * Name or index of related client app. + * Name of related client app. */ - clientApp?: string; + clientProject: string; /** - * Name or index of related universal app. + * Name of related universal app. */ - universalApp: string; + universalProject: string; /** * Route path used to produce the app shell. */ diff --git a/packages/schematics/angular/app-shell/schema.json b/packages/schematics/angular/app-shell/schema.json index cfb3c8d37b..23d2ab69a0 100644 --- a/packages/schematics/angular/app-shell/schema.json +++ b/packages/schematics/angular/app-shell/schema.json @@ -4,13 +4,13 @@ "title": "Angular AppShell Options Schema", "type": "object", "properties": { - "clientApp": { + "clientProject": { "type": "string", - "description": "Name or index of related client app." + "description": "Name of related client app." }, - "universalApp": { + "universalProject": { "type": "string", - "description": "Name or index of related universal app." + "description": "Name of related universal app." }, "route": { "type": "string", @@ -95,10 +95,11 @@ "format": "path", "description": "The path of the source directory.", "default": "src", - "alias": "sd" + "alias": "D" } }, "required": [ - "universalApp" + "clientProject", + "universalProject" ] } diff --git a/packages/schematics/angular/application/files/__dot__angular-cli.json b/packages/schematics/angular/application/files/__dot__angular-cli.json deleted file mode 100644 index 151b05d7d5..0000000000 --- a/packages/schematics/angular/application/files/__dot__angular-cli.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "project": { - "name": "<%= utils.dasherize(name) %>" - }, - "apps": [ - { - "root": "<%= sourceDir %>", - "outDir": "dist", - "assets": [ - "assets", - "favicon.ico" - ], - "index": "index.html", - "main": "main.ts", - "polyfills": "polyfills.ts", - "test": "test.ts", - "tsconfig": "tsconfig.app.json", - "testTsconfig": "tsconfig.spec.json", - "prefix": "<%= prefix %>", - "styles": [ - "styles.<%= style %>" - ], - "scripts": [], - "environmentSource": "environments/environment.ts", - "environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" - }<% if (serviceWorker) { %>, - "serviceWorker": true<% } %> - } - ], - "e2e": { - "protractor": { - "config": "./protractor.conf.js" - } - }, - "lint": [ - { - "project": "<%= sourceDir %>/tsconfig.app.json", - "exclude": "**/node_modules/**" - }, - { - "project": "<%= sourceDir %>/tsconfig.spec.json", - "exclude": "**/node_modules/**" - }, - { - "project": "e2e/tsconfig.e2e.json", - "exclude": "**/node_modules/**" - } - ], - "test": { - "karma": { - "config": "./karma.conf.js" - } - }, - "defaults": { - "styleExt": "<%= style %>",<% if (minimal || skipTests) { %> - "class": { - "spec": false - },<% } %> - "component": {<% if (minimal || inlineStyle) { %> - "inlineStyle": true<% } %><% if (minimal || (inlineTemplate && inlineStyle)) { %>,<% } %><% if (minimal || inlineTemplate) { %> - "inlineTemplate": true<% } %><% if (minimal || (skipTests && (inlineStyle || inlineTemplate))) { %>,<% } %><% if (minimal || skipTests) { %> - "spec": false - <% } %>}<% if (minimal || skipTests) { %>, - "directive": { - "spec": false - }, - "guard": { - "spec": false - }, - "module": { - "spec": false - }, - "pipe": { - "spec": false - }, - "service": { - "spec": false - }<% } %> - } -} diff --git a/packages/schematics/angular/application/files/__sourcedir__/tsconfig.app.json b/packages/schematics/angular/application/files/__sourcedir__/tsconfig.app.json deleted file mode 100644 index 939fe613af..0000000000 --- a/packages/schematics/angular/application/files/__sourcedir__/tsconfig.app.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "<%= sourcedir.split('/').map(x => '..').join('/') %>/tsconfig.json", - "compilerOptions": { - "outDir": "<%= sourcedir.split('/').map(x => '..').join('/') %>/out-tsc/app", - "baseUrl": "./", - "module": "es2015", - "types": [] - }, - "exclude": [ - "test.ts", - "**/*.spec.ts" - ] -} diff --git a/packages/schematics/angular/application/files/__sourcedir__/tsconfig.spec.json b/packages/schematics/angular/application/files/__sourcedir__/tsconfig.spec.json deleted file mode 100644 index 1df99a878c..0000000000 --- a/packages/schematics/angular/application/files/__sourcedir__/tsconfig.spec.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "<%= sourcedir.split('/').map(x => '..').join('/') %>/tsconfig.json", - "compilerOptions": { - "outDir": "<%= sourcedir.split('/').map(x => '..').join('/') %>/out-tsc/spec", - "baseUrl": "./", - "module": "commonjs", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] -} diff --git a/packages/schematics/angular/application/files/lint/__tsLintRoot__/tslint.json b/packages/schematics/angular/application/files/lint/__tsLintRoot__/tslint.json new file mode 100644 index 0000000000..2eec2dd9cc --- /dev/null +++ b/packages/schematics/angular/application/files/lint/__tsLintRoot__/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "<%= relativeTsLintPath %>/tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "<%= prefix %>", + "camelCase" + ], + "component-selector": [ + true, + "element", + "<%= prefix %>", + "kebab-case" + ] + } +} diff --git a/packages/schematics/angular/application/files/package.json b/packages/schematics/angular/application/files/package.json deleted file mode 100644 index 9e808bb67e..0000000000 --- a/packages/schematics/angular/application/files/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "<%= utils.dasherize(name) %>", - "version": "0.0.0", - "license": "MIT", - "scripts": { - "ng": "ng", - "start": "ng serve", - "build": "ng build --prod", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e" - }, - "private": true, - "dependencies": { - "@angular/animations": "^5.2.0", - "@angular/common": "^5.2.0", - "@angular/compiler": "^5.2.0", - "@angular/core": "^5.2.0", - "@angular/forms": "^5.2.0", - "@angular/http": "^5.2.0", - "@angular/platform-browser": "^5.2.0", - "@angular/platform-browser-dynamic": "^5.2.0", - "@angular/router": "^5.2.0",<% if (serviceWorker) { %> - "@angular/service-worker": "^5.2.0",<% } %> - "core-js": "^2.4.1", - "rxjs": "^5.5.6", - "zone.js": "^0.8.19" - }, - "devDependencies": { - "@angular/cli": "~<%= version %>", - "@angular/compiler-cli": "^5.2.0", - "@angular/language-service": "^5.2.0",<% if (!minimal) { %> - "@types/jasmine": "~2.8.6", - "@types/jasminewd2": "~2.0.3", - "@types/node": "~8.9.4", - "codelyzer": "~4.1.0", - "jasmine-core": "~2.99.1", - "jasmine-spec-reporter": "~4.2.1", - "karma": "~2.0.0", - "karma-chrome-launcher": "~2.2.0", - "karma-coverage-istanbul-reporter": "~1.4.1", - "karma-jasmine": "~1.1.1", - "karma-jasmine-html-reporter": "^0.2.2", - "protractor": "~5.3.0", - "ts-node": "~5.0.0", - "tslint": "~5.9.1",<% } %> - "typescript": "~2.5.3" - } -} diff --git a/packages/schematics/angular/application/files/root/browserslist b/packages/schematics/angular/application/files/root/browserslist new file mode 100644 index 0000000000..1b1be90441 --- /dev/null +++ b/packages/schematics/angular/application/files/root/browserslist @@ -0,0 +1,5 @@ +> 0.5% +last 2 versions +Firefox ESR +not dead +IE 9-11 \ No newline at end of file diff --git a/packages/schematics/angular/application/files/root/karma.conf.js b/packages/schematics/angular/application/files/root/karma.conf.js new file mode 100644 index 0000000000..15b5b0d5f7 --- /dev/null +++ b/packages/schematics/angular/application/files/root/karma.conf.js @@ -0,0 +1,34 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, 'coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + angularCli: { + environment: 'dev' + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git a/packages/schematics/angular/application/files/root/tsconfig.app.json b/packages/schematics/angular/application/files/root/tsconfig.app.json new file mode 100644 index 0000000000..d4e6e46ada --- /dev/null +++ b/packages/schematics/angular/application/files/root/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "<%= relativeTsConfigPath %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= relativeTsConfigPath %>/out-tsc/app", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/packages/schematics/angular/application/files/root/tsconfig.spec.json b/packages/schematics/angular/application/files/root/tsconfig.spec.json new file mode 100644 index 0000000000..975342957b --- /dev/null +++ b/packages/schematics/angular/application/files/root/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "<%= relativeTsConfigPath %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= relativeTsConfigPath %>/out-tsc/spec", + "module": "commonjs", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "<%= rootInSrc ? '' : 'src/' %>test.ts", + "<%= rootInSrc ? '' : 'src/' %>polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/packages/schematics/angular/application/files/__sourcedir__/assets/__dot__gitkeep b/packages/schematics/angular/application/files/src/assets/__dot__gitkeep similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/assets/__dot__gitkeep rename to packages/schematics/angular/application/files/src/assets/__dot__gitkeep diff --git a/packages/schematics/angular/application/files/__sourcedir__/environments/environment.prod.ts b/packages/schematics/angular/application/files/src/environments/environment.prod.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/environments/environment.prod.ts rename to packages/schematics/angular/application/files/src/environments/environment.prod.ts diff --git a/packages/schematics/angular/application/files/__sourcedir__/environments/environment.ts b/packages/schematics/angular/application/files/src/environments/environment.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/environments/environment.ts rename to packages/schematics/angular/application/files/src/environments/environment.ts diff --git a/packages/schematics/angular/application/files/__sourcedir__/favicon.ico b/packages/schematics/angular/application/files/src/favicon.ico similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/favicon.ico rename to packages/schematics/angular/application/files/src/favicon.ico diff --git a/packages/schematics/angular/application/files/__sourcedir__/index.html b/packages/schematics/angular/application/files/src/index.html similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/index.html rename to packages/schematics/angular/application/files/src/index.html diff --git a/packages/schematics/angular/application/files/__sourcedir__/main.ts b/packages/schematics/angular/application/files/src/main.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/main.ts rename to packages/schematics/angular/application/files/src/main.ts diff --git a/packages/schematics/angular/application/files/__sourcedir__/polyfills.ts b/packages/schematics/angular/application/files/src/polyfills.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/polyfills.ts rename to packages/schematics/angular/application/files/src/polyfills.ts diff --git a/packages/schematics/angular/application/files/__sourcedir__/styles.__style__ b/packages/schematics/angular/application/files/src/styles.__style__ similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/styles.__style__ rename to packages/schematics/angular/application/files/src/styles.__style__ diff --git a/packages/schematics/angular/application/files/__sourcedir__/test.ts b/packages/schematics/angular/application/files/src/test.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/test.ts rename to packages/schematics/angular/application/files/src/test.ts diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 6e0db4e4a5..334755bb34 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -5,11 +5,13 @@ * Use 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 { strings } from '@angular-devkit/core'; +import { JsonObject, normalize, relative, strings, tags } from '@angular-devkit/core'; +import { experimental } from '@angular-devkit/core'; import { MergeStrategy, Rule, SchematicContext, + SchematicsException, Tree, apply, chain, @@ -21,72 +23,363 @@ import { template, url, } from '@angular-devkit/schematics'; -import { - NodePackageInstallTask, - NodePackageLinkTask, - RepositoryInitializerTask, -} from '@angular-devkit/schematics/tasks'; +import { Schema as E2eOptions } from '../e2e/schema'; +import { getWorkspace, getWorkspacePath } from '../utility/config'; +import { latestVersions } from '../utility/latest-versions'; import { Schema as ApplicationOptions } from './schema'; +type WorkspaceSchema = experimental.workspace.WorkspaceSchema; + +// TODO: use JsonAST +// function appendPropertyInAstObject( +// recorder: UpdateRecorder, +// node: JsonAstObject, +// propertyName: string, +// value: JsonValue, +// indent = 4, +// ) { +// const indentStr = '\n' + new Array(indent + 1).join(' '); + +// if (node.properties.length > 0) { +// // Insert comma. +// const last = node.properties[node.properties.length - 1]; +// recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ','); +// } + +// recorder.insertLeft( +// node.end.offset - 1, +// ' ' +// + `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}` +// + indentStr.slice(0, -2), +// ); +// } -function minimalPathFilter(path: string): boolean { - const toRemoveList: RegExp[] = [/e2e\//, /editorconfig/, /README/, /karma.conf.js/, - /protractor.conf.js/, /test.ts/, /tsconfig.spec.json/, - /tslint.json/, /favicon.ico/]; +function addDependenciesToPackageJson() { + return (host: Tree) => { + const packageJsonPath = 'package.json'; - return !toRemoveList.some(re => re.test(path)); + if (!host.exists('package.json')) { return host; } + + const source = host.read('package.json'); + if (!source) { return host; } + + const sourceText = source.toString('utf-8'); + const json = JSON.parse(sourceText); + + if (!json['devDependencies']) { + json['devDependencies'] = {}; + } + + json.devDependencies = { + '@angular/compiler-cli': latestVersions.Angular, + '@angular-devkit/build-angular': latestVersions.DevkitBuildAngular, + 'typescript': latestVersions.TypeScript, + // De-structure last keeps existing user dependencies. + ...json.devDependencies, + }; + + host.overwrite(packageJsonPath, JSON.stringify(json, null, 2)); + + return host; + }; +} + +function addAppToWorkspaceFile(options: ApplicationOptions, workspace: WorkspaceSchema): Rule { + return (host: Tree, context: SchematicContext) => { + // TODO: use JsonAST + // const workspacePath = '/angular.json'; + // const workspaceBuffer = host.read(workspacePath); + // if (workspaceBuffer === null) { + // throw new SchematicsException(`Configuration file (${workspacePath}) not found.`); + // } + // const workspaceJson = parseJson(workspaceBuffer.toString()); + // if (workspaceJson.value === null) { + // throw new SchematicsException(`Unable to parse configuration file (${workspacePath}).`); + // } + let projectRoot = options.projectRoot !== undefined + ? options.projectRoot + : `${workspace.newProjectRoot}/${options.name}`; + if (projectRoot !== '' && !projectRoot.endsWith('/')) { + projectRoot += '/'; + } + const rootFilesRoot = options.projectRoot === undefined + ? projectRoot + : projectRoot + 'src/'; + + // tslint:disable-next-line:no-any + const project: any = { + root: projectRoot, + projectType: 'application', + prefix: options.prefix || 'app', + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + options: { + outputPath: `dist/${options.name}`, + index: `${projectRoot}src/index.html`, + main: `${projectRoot}src/main.ts`, + polyfills: `${projectRoot}src/polyfills.ts`, + tsConfig: `${rootFilesRoot}tsconfig.app.json`, + assets: [ + { + glob: 'favicon.ico', + input: `${projectRoot}src`, + output: '/', + }, + { + glob: '**/*', + input: `${projectRoot}src/assets`, + output: '/assets', + }, + ], + styles: [ + { + input: `${projectRoot}src/styles.${options.style}`, + }, + ], + scripts: [], + }, + configurations: { + production: { + fileReplacements: [{ + replace: `${projectRoot}src/environments/environment.ts`, + with: `${projectRoot}src/environments/environment.prod.ts`, + }], + optimization: true, + outputHashing: 'all', + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + }, + }, + }, + serve: { + builder: '@angular-devkit/build-angular:dev-server', + options: { + browserTarget: `${options.name}:build`, + }, + configurations: { + production: { + browserTarget: `${options.name}:build:production`, + }, + }, + }, + 'extract-i18n': { + builder: '@angular-devkit/build-angular:extract-i18n', + options: { + browserTarget: `${options.name}:build`, + }, + }, + test: { + builder: '@angular-devkit/build-angular:karma', + options: { + main: `${projectRoot}src/test.ts`, + polyfills: `${projectRoot}src/polyfills.ts`, + tsConfig: `${rootFilesRoot}tsconfig.spec.json`, + karmaConfig: `${rootFilesRoot}karma.conf.js`, + styles: [ + { + input: `${projectRoot}styles.${options.style}`, + }, + ], + scripts: [], + assets: [ + { + glob: 'favicon.ico', + input: `${projectRoot}src/`, + output: '/', + }, + { + glob: '**/*', + input: `${projectRoot}src/assets`, + output: '/assets', + }, + ], + }, + }, + lint: { + builder: '@angular-devkit/build-angular:tslint', + options: { + tsConfig: [ + `${rootFilesRoot}tsconfig.app.json`, + `${rootFilesRoot}tsconfig.spec.json`, + ], + exclude: [ + '**/node_modules/**', + ], + }, + }, + }, + }; + // tslint:disable-next-line:no-any + // const projects: JsonObject = ( workspaceAst.value).projects || {}; + // tslint:disable-next-line:no-any + // if (!( workspaceAst.value).projects) { + // // tslint:disable-next-line:no-any + // ( workspaceAst.value).projects = projects; + // } + + workspace.projects[options.name] = project; + + const schematics: JsonObject = {}; + + if (options.inlineTemplate === true + || options.inlineStyle === true + || options.style !== undefined) { + schematics['@schematics/angular:component'] = {}; + if (options.inlineTemplate === true) { + (schematics['@schematics/angular:component'] as JsonObject).inlineTemplate = true; + } + if (options.inlineStyle === true) { + (schematics['@schematics/angular:component'] as JsonObject).inlineStyle = true; + } + if (options.style !== undefined) { + (schematics['@schematics/angular:component'] as JsonObject).styleext = options.style; + } + } + + if (options.skipTests === true) { + ['class', 'component', 'directive', 'guard', 'module', 'pipe', 'service'].forEach((type) => { + if (!(`@schematics/angular:${type}` in schematics)) { + schematics[`@schematics/angular:${type}`] = {}; + } + (schematics[`@schematics/angular:${type}`] as JsonObject).spec = false; + }); + } + + workspace.schematics = schematics; + + host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2)); + }; } +const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/; +const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app']; + +function getRegExpFailPosition(str: string): number | null { + const parts = str.indexOf('-') >= 0 ? str.split('-') : [str]; + const matched: string[] = []; + + parts.forEach(part => { + if (part.match(projectNameRegexp)) { + matched.push(part); + } + }); + + const compare = matched.join('-'); + + return (str !== compare) ? compare.length : null; +} + +function validateProjectName(projectName: string) { + const errorIndex = getRegExpFailPosition(projectName); + if (errorIndex !== null) { + const firstMessage = tags.oneLine` + Project name "${projectName}" is not valid. New project names must + start with a letter, and must contain only alphanumeric characters or dashes. + When adding a dash the segment after the dash must also start with a letter. + `; + const msg = tags.stripIndent` + ${firstMessage} + ${projectName} + ${Array(errorIndex + 1).join(' ') + '^'} + `; + throw new SchematicsException(msg); + } else if (unsupportedProjectNames.indexOf(projectName) !== -1) { + throw new SchematicsException(`Project name "${projectName}" is not a supported name.`); + } + +} + export default function (options: ApplicationOptions): Rule { return (host: Tree, context: SchematicContext) => { - const appRootSelector = `${options.prefix}-root`; - const componentOptions = !options.minimal ? - { - inlineStyle: options.inlineStyle, - inlineTemplate: options.inlineTemplate, - spec: !options.skipTests, - styleext: options.style, - } : - { - inlineStyle: true, - inlineTemplate: true, - spec: false, - styleext: options.style, - }; - const sourceDir = options.sourceDir || 'src'; - - let packageTask; - if (!options.skipInstall) { - packageTask = context.addTask(new NodePackageInstallTask(options.directory)); - if (options.linkCli) { - packageTask = context.addTask( - new NodePackageLinkTask('@angular/cli', options.directory), - [packageTask], - ); + if (!options.name) { + throw new SchematicsException(`Invalid options, "name" is required.`); + } + validateProjectName(options.name); + const prefix = options.prefix || 'app'; + const appRootSelector = `${prefix}-root`; + const componentOptions = { + inlineStyle: options.inlineStyle, + inlineTemplate: options.inlineTemplate, + spec: !options.skipTests, + styleext: options.style, + viewEncapsulation: options.viewEncapsulation, + }; + + const workspace = getWorkspace(host); + let newProjectRoot = workspace.newProjectRoot; + let appDir = `${newProjectRoot}/${options.name}`; + let sourceRoot = `${appDir}/src`; + let sourceDir = `${sourceRoot}/app`; + let relativeTsConfigPath = appDir.split('/').map(x => '..').join('/'); + let relativeTsLintPath = appDir.split('/').map(x => '..').join('/'); + const rootInSrc = options.projectRoot !== undefined; + if (options.projectRoot !== undefined) { + newProjectRoot = options.projectRoot; + appDir = `${newProjectRoot}/src`; + sourceRoot = appDir; + sourceDir = `${sourceRoot}/app`; + relativeTsConfigPath = relative(normalize('/' + sourceRoot), normalize('/')); + if (relativeTsConfigPath === '') { + relativeTsConfigPath = '.'; + } + relativeTsLintPath = relative(normalize('/' + sourceRoot), normalize('/')); + if (relativeTsLintPath === '') { + relativeTsLintPath = '.'; } } - if (!options.skipGit) { - context.addTask( - new RepositoryInitializerTask( - options.directory, - options.commit, - ), - packageTask ? [packageTask] : [], - ); + const tsLintRoot = appDir; + + const e2eOptions: E2eOptions = { + name: `${options.name}-e2e`, + relatedAppName: options.name, + rootSelector: appRootSelector, + }; + if (options.projectRoot !== undefined) { + e2eOptions.projectRoot = 'e2e'; } return chain([ + addAppToWorkspaceFile(options, workspace), + options.skipPackageJson ? noop() : addDependenciesToPackageJson(), + mergeWith( + apply(url('./files/src'), [ + template({ + utils: strings, + ...options, + 'dot': '.', + relativeTsConfigPath, + }), + move(sourceRoot), + ])), mergeWith( - apply(url('./files'), [ - options.minimal ? filter(minimalPathFilter) : noop(), - options.skipGit ? filter(path => !path.endsWith('/__dot__gitignore')) : noop(), - options.serviceWorker ? noop() : filter(path => !path.endsWith('/ngsw-config.json')), + apply(url('./files/root'), [ template({ utils: strings, ...options, 'dot': '.', - sourcedir: sourceDir, + relativeTsConfigPath, + rootInSrc, + }), + move(appDir), + ])), + mergeWith( + apply(url('./files/lint'), [ + template({ + utils: strings, + ...options, + tsLintRoot, + relativeTsLintPath, + prefix, }), - move(options.directory), + // TODO: Moving should work but is bugged right now. + // The __tsLintRoot__ is being used meanwhile. + // Otherwise the tslint.json file could be inside of the root folder and + // this block and the lint folder could be removed. ])), schematic('module', { name: 'app', @@ -94,16 +387,14 @@ export default function (options: ApplicationOptions): Rule { flat: true, routing: options.routing, routingScope: 'Root', - path: options.path, - sourceDir: options.directory + '/' + sourceDir, + path: sourceDir, spec: false, }), schematic('component', { name: 'app', selector: appRootSelector, - sourceDir: options.directory + '/' + sourceDir, flat: true, - path: options.path, + path: sourceDir, skipImport: true, ...componentOptions, }), @@ -117,8 +408,9 @@ export default function (options: ApplicationOptions): Rule { selector: appRootSelector, ...componentOptions, }), - move(options.directory + '/' + sourceDir + '/app'), + move(sourceDir), ]), MergeStrategy.Overwrite), + schematic('e2e', e2eOptions), ])(host, context); }; } diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 723e8391f1..018816559f 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -7,111 +7,231 @@ */ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { getFileContent } from '../utility/test'; +import { latestVersions } from '../utility/latest-versions'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ApplicationOptions } from './schema'; - +// tslint:disable:max-line-length describe('Application Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', path.join(__dirname, '../collection.json'), ); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + const defaultOptions: ApplicationOptions = { - directory: 'foo', name: 'foo', - sourceDir: 'src', inlineStyle: false, inlineTemplate: false, - viewEncapsulation: 'Emulated', - version: '1.2.3', routing: false, style: 'css', skipTests: false, - minimal: false, + skipPackageJson: false, }; + let workspaceTree: UnitTestTree; + beforeEach(() => { + workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions); + }); + it('should create all files of an application', () => { const options = { ...defaultOptions }; - const tree = schematicRunner.runSchematic('application', options); + const tree = schematicRunner.runSchematic('application', options, workspaceTree); const files = tree.files; - expect(files.indexOf('/foo/.editorconfig')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/.angular-cli.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/.gitignore')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/karma.conf.js')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/package.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/protractor.conf.js')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/README.md')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/tsconfig.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/tslint.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/e2e/app.e2e-spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/e2e/app.po.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/favicon.ico')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/index.html')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/main.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/polyfills.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/styles.css')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/test.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/tsconfig.app.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/tsconfig.spec.json')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/typings.d.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/assets/.gitkeep')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/environments/environment.prod.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/environments/environment.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app.component.css')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app.component.html')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app.component.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/karma.conf.js')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/tsconfig.app.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/tsconfig.spec.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/tslint.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/environments/environment.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/environments/environment.prod.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/favicon.ico')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/index.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/main.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/polyfills.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/styles.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/test.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app.component.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app.component.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app.component.ts')).toBeGreaterThanOrEqual(0); }); - it('should handle a different sourceDir', () => { - const options = { ...defaultOptions, sourceDir: 'some/custom/path' }; + it('should add the application to the workspace', () => { + const options = { ...defaultOptions }; - let tree: UnitTestTree | null = null; - expect(() => tree = schematicRunner.runSchematic('application', options)) - .not.toThrow(); + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo).toBeDefined(); + }); + + it('should set the prefix to app if none is set', () => { + const options = { ...defaultOptions }; - if (tree) { - // tslint:disable-next-line:non-null-operator - const files = tree !.files; - expect(files.indexOf('/foo/some/custom/path/app/app.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/some/custom/path/tsconfig.app.json')).toBeGreaterThanOrEqual(0); - } + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo.prefix).toEqual('app'); + }); + + it('should set the prefix correctly', () => { + const options = { ...defaultOptions, prefix: 'pre' }; + + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo.prefix).toEqual('pre'); }); it('should handle the routing flag', () => { const options = { ...defaultOptions, routing: true }; - const tree = schematicRunner.runSchematic('application', options); + const tree = schematicRunner.runSchematic('application', options, workspaceTree); const files = tree.files; - expect(files.indexOf('/foo/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/foo/src/app/app-routing.module.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/foo/src/app/app.module.ts'); + expect(files.indexOf('/projects/foo/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app/app-routing.module.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = tree.readContent('/projects/foo/src/app/app.module.ts'); expect(moduleContent).toMatch(/import { AppRoutingModule } from '.\/app-routing.module'/); - const routingModuleContent = getFileContent(tree, '/foo/src/app/app-routing.module.ts'); + const routingModuleContent = tree.readContent('/projects/foo/src/app/app-routing.module.ts'); expect(routingModuleContent).toMatch(/RouterModule.forRoot\(routes\)/); }); - it('should handle the skip git flag', () => { - const options = { ...defaultOptions, skipGit: true }; - - const tree = schematicRunner.runSchematic('application', options); - const files = tree.files; - expect(files.indexOf('/foo/.gitignore')).toEqual(-1); - }); - it('should import BrowserModule in the app module', () => { - const tree = schematicRunner.runSchematic('application', defaultOptions); - const path = '/foo/src/app/app.module.ts'; + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + const path = '/projects/foo/src/app/app.module.ts'; const content = tree.readContent(path); expect(content).toMatch(/import { BrowserModule } from \'@angular\/platform-browser\';/); }); it('should declare app component in the app module', () => { - const tree = schematicRunner.runSchematic('application', defaultOptions); - const path = '/foo/src/app/app.module.ts'; + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + const path = '/projects/foo/src/app/app.module.ts'; const content = tree.readContent(path); expect(content).toMatch(/import { AppComponent } from \'\.\/app\.component\';/); }); + + it('should set the right paths in the tsconfig files', () => { + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + let path = '/projects/foo/tsconfig.app.json'; + let content = tree.readContent(path); + expect(content).toMatch('../../tsconfig.json'); + path = '/projects/foo/tsconfig.spec.json'; + content = tree.readContent(path); + expect(content).toMatch('../../tsconfig.json'); + const specTsConfig = JSON.parse(content); + expect(specTsConfig.files).toEqual(['src/test.ts', 'src/polyfills.ts']); + }); + + it('should set the right path and prefix in the tslint file', () => { + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + const path = '/projects/foo/tslint.json'; + const content = JSON.parse(tree.readContent(path)); + expect(content.extends).toMatch('../../tslint.json'); + expect(content.rules['directive-selector'][2]).toMatch('app'); + expect(content.rules['component-selector'][2]).toMatch('app'); + }); + + describe(`update package.json`, () => { + it(`should add build-angular to devDependencies`, () => { + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + + const packageJson = JSON.parse(tree.readContent('package.json')); + expect(packageJson.devDependencies['@angular-devkit/build-angular']) + .toEqual(latestVersions.DevkitBuildAngular); + }); + + it('should use the latest known versions in package.json', () => { + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@angular/compiler-cli']).toEqual(latestVersions.Angular); + expect(pkg.devDependencies['typescript']).toEqual(latestVersions.TypeScript); + }); + + it(`should not override existing users dependencies`, () => { + const oldPackageJson = workspaceTree.readContent('package.json'); + workspaceTree.overwrite('package.json', oldPackageJson.replace( + `"typescript": "${latestVersions.TypeScript}"`, + `"typescript": "~2.5.2"`, + )); + + const tree = schematicRunner.runSchematic('application', defaultOptions, workspaceTree); + const packageJson = JSON.parse(tree.readContent('package.json')); + expect(packageJson.devDependencies.typescript).toEqual('~2.5.2'); + }); + + it(`should not modify the file when --skipPackageJson`, () => { + const tree = schematicRunner.runSchematic('application', { + name: 'foo', + skipPackageJson: true, + }, workspaceTree); + + const packageJson = JSON.parse(tree.readContent('package.json')); + expect(packageJson.devDependencies['@angular-devkit/build-angular']).toBeUndefined(); + }); + }); + + describe('custom projectRoot', () => { + it('should put app files in the right spot', () => { + const options = { ...defaultOptions, projectRoot: '' }; + + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const files = tree.files; + expect(files.indexOf('/src/karma.conf.js')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/tsconfig.app.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/tsconfig.spec.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/tslint.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/environments/environment.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/environments/environment.prod.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/favicon.ico')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/index.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/main.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/polyfills.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/styles.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/test.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/app/app.component.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/app/app.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/app/app.component.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/src/app/app.component.ts')).toBeGreaterThanOrEqual(0); + }); + + it('should set values in angular.json correctly', () => { + const options = { ...defaultOptions, projectRoot: '' }; + + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const config = JSON.parse(tree.readContent('/angular.json')); + const prj = config.projects.foo; + expect(prj.root).toEqual(''); + const buildOpt = prj.architect.build.options; + expect(buildOpt.index).toEqual('src/index.html'); + expect(buildOpt.main).toEqual('src/main.ts'); + expect(buildOpt.polyfills).toEqual('src/polyfills.ts'); + expect(buildOpt.tsConfig).toEqual('src/tsconfig.app.json'); + }); + + it('should set the relative tsconfig paths', () => { + const options = { ...defaultOptions, projectRoot: '' }; + + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const appTsConfig = JSON.parse(tree.readContent('/src/tsconfig.app.json')); + expect(appTsConfig.extends).toEqual('../tsconfig.json'); + const specTsConfig = JSON.parse(tree.readContent('/src/tsconfig.spec.json')); + expect(specTsConfig.extends).toEqual('../tsconfig.json'); + expect(specTsConfig.files).toEqual(['test.ts', 'polyfills.ts']); + }); + + it('should set the relative path and prefix in the tslint file', () => { + const options = { ...defaultOptions, projectRoot: '' }; + + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + const content = JSON.parse(tree.readContent('/src/tslint.json')); + expect(content.extends).toMatch('../tslint.json'); + expect(content.rules['directive-selector'][2]).toMatch('app'); + expect(content.rules['component-selector'][2]).toMatch('app'); + }); + }); }); diff --git a/packages/schematics/angular/application/other-files/app.module.ts b/packages/schematics/angular/application/other-files/app.module.ts index 2ba765db55..2c4bc05c64 100644 --- a/packages/schematics/angular/application/other-files/app.module.ts +++ b/packages/schematics/angular/application/other-files/app.module.ts @@ -2,11 +2,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; <% if (routing) { %> import { AppRoutingModule } from './app-routing.module';<% } %> -<% if (serviceWorker) { %> -import { ServiceWorkerModule } from '@angular/service-worker';<% } %> import { AppComponent } from './app.component'; -<% if (serviceWorker) { %> -import { environment } from '../environments/environment';<% } %> @NgModule({ declarations: [ @@ -14,8 +10,7 @@ import { environment } from '../environments/environment';<% } %> ], imports: [ BrowserModule<% if (routing) { %>, - AppRoutingModule<% } %><% if (serviceWorker) { %>, - ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production })<% } %> + AppRoutingModule<% } %> ], providers: [], bootstrap: [AppComponent] diff --git a/packages/schematics/angular/application/schema.d.ts b/packages/schematics/angular/application/schema.d.ts index 091f548110..94b4b3faed 100644 --- a/packages/schematics/angular/application/schema.d.ts +++ b/packages/schematics/angular/application/schema.d.ts @@ -8,17 +8,9 @@ export interface Schema { /** - * The directory name to create the app in. + * The root directory of the new application. */ - directory: string; - /** - * The path of the application. - */ - path?: string; - /** - * The path of the source directory. - */ - sourceDir?: string; + projectRoot?: string; /** * The name of the application. */ @@ -35,10 +27,6 @@ export interface Schema { * Specifies the view encapsulation strategy. */ viewEncapsulation?: ('Emulated' | 'Native' | 'None'); - /** - * The version of the Angular CLI to use. - */ - version?: string; /** * Generates a routing module. */ @@ -56,27 +44,7 @@ export interface Schema { */ skipTests?: boolean; /** - * Skip installing dependency packages. - */ - skipInstall?: boolean; - /** - * Link CLI to global version (internal development only). - */ - linkCli?: boolean; - /** - * Skip initializing a git repository. - */ - skipGit?: boolean; - /** - * Initial repository commit information. - */ - commit?: { name: string, email: string, message?: string }; - /** - * Create a minimal app (no test structure, inline styles/templates). - */ - minimal?: boolean; - /** - * Installs the @angular/service-worker. - */ - serviceWorker?: boolean; + * Do not add dependencies to package.json (e.g., --skipPackageJson) + */ + skipPackageJson: boolean; } diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index 2166263131..0bf4bd5469 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -4,54 +4,37 @@ "title": "Angular Application Options Schema", "type": "object", "properties": { - "directory": { + "projectRoot": { + "description": "The root directory of the new application.", "type": "string", - "format": "path", - "description": "The directory name to create the app in.", - "alias": "dir" - }, - "path": { - "type": "string", - "format": "path", - "description": "The path of the application.", - "default": "app", - "visible": false - }, - "sourceDir": { - "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "alias": "sd", "visible": false }, "name": { "description": "The name of the application.", "type": "string", - "format": "html-selector" + "format": "html-selector", + "$default": { + "$source": "argv", + "index": 0 + } }, "inlineStyle": { "description": "Specifies if the style will be in the ts file.", "type": "boolean", "default": false, - "alias": "is" + "alias": "s" }, "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", "default": false, - "alias": "it" + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", "enum": ["Emulated", "Native", "None"], "type": "string" }, - "version": { - "type": "string", - "description": "The version of the Angular CLI to use.", - "visible": false - }, "routing": { "type": "boolean", "description": "Generates a routing module.", @@ -73,64 +56,13 @@ "description": "Skip creating spec files.", "type": "boolean", "default": false, - "alias": "st" - }, - "skipInstall": { - "description": "Skip installing dependency packages.", - "type": "boolean", - "default": false - }, - "linkCli": { - "description": "Link CLI to global version (internal development only).", - "type": "boolean", - "default": false, - "visible": false + "alias": "S" }, - "skipGit": { - "description": "Skip initializing a git repository.", + "skipPackageJson": { "type": "boolean", "default": false, - "alias": "sg" - }, - "commit": { - "description": "Initial repository commit information.", - "default": null, - "oneOf": [ - { "type": "null" }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "message": { - "type": "string" - } - }, - "required": [ - "name", - "email" - ] - } - ] - }, - "minimal": { - "description": "Create a minimal app (no test structure, inline styles/templates).", - "type": "boolean", - "default": false - }, - "serviceWorker": { - "description": "Installs the @angular/service-worker.", - "type": "boolean", - "default": false + "description": "Do not add dependencies to package.json." } }, - "required": [ - "name", - "directory" - ] + "required": [] } diff --git a/packages/schematics/angular/class/files/__path__/__name@dasherize____type__.spec.ts b/packages/schematics/angular/class/files/__name@dasherize____type__.spec.ts similarity index 100% rename from packages/schematics/angular/class/files/__path__/__name@dasherize____type__.spec.ts rename to packages/schematics/angular/class/files/__name@dasherize____type__.spec.ts diff --git a/packages/schematics/angular/class/files/__path__/__name@dasherize____type__.ts b/packages/schematics/angular/class/files/__name@dasherize____type__.ts similarity index 100% rename from packages/schematics/angular/class/files/__path__/__name@dasherize____type__.ts rename to packages/schematics/angular/class/files/__name@dasherize____type__.ts diff --git a/packages/schematics/angular/class/index.ts b/packages/schematics/angular/class/index.ts index 484ed2bc8f..058c6805e6 100644 --- a/packages/schematics/angular/class/index.ts +++ b/packages/schematics/angular/class/index.ts @@ -5,13 +5,13 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, - SchematicsException, + SchematicContext, + Tree, apply, branchAndMerge, - chain, filter, mergeWith, move, @@ -19,29 +19,37 @@ import { template, url, } from '@angular-devkit/schematics'; +import { getWorkspace } from '../utility/config'; +import { parseName } from '../utility/parse-name'; import { Schema as ClassOptions } from './schema'; - export default function (options: ClassOptions): Rule { - options.type = !!options.type ? `.${options.type}` : ''; - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + + options.type = !!options.type ? `.${options.type}` : ''; + + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; - const templateSource = apply(url('./files'), [ - options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), - template({ - ...strings, - ...options, - }), - move(sourceDir), - ]); + const templateSource = apply(url('./files'), [ + options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), + template({ + ...strings, + ...options, + }), + move(parsedPath.path), + ]); - return chain([ - branchAndMerge(chain([ - mergeWith(templateSource), - ])), - ]); + return branchAndMerge(mergeWith(templateSource))(host, context); + }; } diff --git a/packages/schematics/angular/class/index_spec.ts b/packages/schematics/angular/class/index_spec.ts index 2de4594d93..694e9573ff 100644 --- a/packages/schematics/angular/class/index_spec.ts +++ b/packages/schematics/angular/class/index_spec.ts @@ -5,9 +5,10 @@ * Use 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 { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ClassOptions } from './schema'; @@ -18,51 +19,72 @@ describe('Class Schematic', () => { ); const defaultOptions: ClassOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', type: '', spec: false, }; - it('should create one file', () => { - const tree = schematicRunner.runSchematic('class', defaultOptions); - expect(tree.files.length).toEqual(1); - expect(tree.files[0]).toEqual('/src/app/foo.ts'); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; + beforeEach(() => { + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create two files if spec is true', () => { + it('should create just the class file', () => { + const tree = schematicRunner.runSchematic('class', defaultOptions, appTree); + expect(tree.files.indexOf('/projects/bar/src/app/foo.ts')).toBeGreaterThanOrEqual(0); + expect(tree.files.indexOf('/projects/bar/src/app/foo.spec.ts')).toBeLessThan(0); + }); + + it('should create the class and spec file', () => { const options = { ...defaultOptions, spec: true, }; - const tree = schematicRunner.runSchematic('class', options); - expect(tree.files.length).toEqual(2); - expect(tree.files.indexOf('/src/app/foo.spec.ts')).toBeGreaterThanOrEqual(0); - expect(tree.files.indexOf('/src/app/foo.ts')).toBeGreaterThanOrEqual(0); + const tree = schematicRunner.runSchematic('class', options, appTree); + expect(tree.files.indexOf('/projects/bar/src/app/foo.ts')).toBeGreaterThanOrEqual(0); + expect(tree.files.indexOf('/projects/bar/src/app/foo.spec.ts')).toBeGreaterThanOrEqual(0); }); it('should create an class named "Foo"', () => { - const tree = schematicRunner.runSchematic('class', defaultOptions); - const fileEntry = tree.get(tree.files[0]); - if (fileEntry) { - const fileContent = fileEntry.content.toString(); - expect(fileContent).toMatch(/export class Foo/); - } + const tree = schematicRunner.runSchematic('class', defaultOptions, appTree); + const fileContent = tree.readContent('/projects/bar/src/app/foo.ts'); + expect(fileContent).toMatch(/export class Foo/); }); it('should put type in the file name', () => { const options = { ...defaultOptions, type: 'model' }; - const tree = schematicRunner.runSchematic('class', options); - expect(tree.files[0]).toEqual('/src/app/foo.model.ts'); + const tree = schematicRunner.runSchematic('class', options, appTree); + expect(tree.files.indexOf('/projects/bar/src/app/foo.model.ts')).toBeGreaterThanOrEqual(0); }); it('should split the name to name & type with split on "."', () => { const options = {...defaultOptions, name: 'foo.model' }; - const tree = schematicRunner.runSchematic('class', options); - expect(tree.files.length).toEqual(1); - expect(tree.files[0]).toEqual('/src/app/foo.model.ts'); - const content = getFileContent(tree, '/src/app/foo.model.ts'); + const tree = schematicRunner.runSchematic('class', options, appTree); + const classPath = '/projects/bar/src/app/foo.model.ts'; + const content = tree.readContent(classPath); expect(content).toMatch(/export class Foo/); }); + + it('should respect the path option', () => { + const options = { ...defaultOptions, path: 'zzz' }; + const tree = schematicRunner.runSchematic('class', options, appTree); + expect(tree.files.indexOf('/zzz/foo.ts')).toBeGreaterThanOrEqual(0); + }); }); diff --git a/packages/schematics/angular/class/schema.d.ts b/packages/schematics/angular/class/schema.d.ts index 9aa8015dfd..95c37cc3d8 100644 --- a/packages/schematics/angular/class/schema.d.ts +++ b/packages/schematics/angular/class/schema.d.ts @@ -16,13 +16,9 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * Specifies if a spec file is generated. */ diff --git a/packages/schematics/angular/class/schema.json b/packages/schematics/angular/class/schema.json index 1cc74bf93c..9e257aaaed 100644 --- a/packages/schematics/angular/class/schema.json +++ b/packages/schematics/angular/class/schema.json @@ -6,26 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the class." + "description": "The name of the class.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", "description": "The path to create the class.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "spec": { @@ -39,7 +34,5 @@ "default": "" } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index 1230c6a1db..79fe1b9ec4 100644 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -1,10 +1,34 @@ { "schematics": { + "ng-new": { + "factory": "./ng-new", + "schema": "./ng-new/schema.json", + "description": "Create an Angular workspace.", + "hidden": true + }, + "workspace": { + "factory": "./workspace", + "schema": "./workspace/schema.json", + "description": "Create an Angular workspace.", + "hidden": true + }, + "serviceWorker": { + "aliases": [ "service-worker" ], + "factory": "./service-worker", + "description": "Initializes a service worker setup.", + "schema": "./service-worker/schema.json" + }, "application": { "factory": "./application", "schema": "./application/schema.json", "description": "Create an Angular application." }, + "e2e": { + "factory": "./e2e", + "schema": "./e2e/schema.json", + "description": "Create an Angular e2e application.", + "hidden": true + }, "class": { "aliases": [ "cl" ], "factory": "./class", @@ -69,6 +93,12 @@ "factory": "./app-shell", "description": "Create an app shell.", "schema": "./app-shell/schema.json" + }, + "library": { + "aliases": ["lib"], + "factory": "./library", + "schema": "./library/schema.json", + "description": "Generate a library project for Angular." } } } diff --git a/packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ similarity index 100% rename from packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ rename to packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.__styleext__ diff --git a/packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html similarity index 100% rename from packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html rename to packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.html diff --git a/packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts similarity index 100% rename from packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts rename to packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts diff --git a/packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.ts b/packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts similarity index 100% rename from packages/schematics/angular/component/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.ts rename to packages/schematics/angular/component/files/__name@dasherize@if-flat__/__name@dasherize__.component.ts diff --git a/packages/schematics/angular/component/index.ts b/packages/schematics/angular/component/index.ts index f1648a60ea..026f3a1379 100644 --- a/packages/schematics/angular/component/index.ts +++ b/packages/schematics/angular/component/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -24,7 +24,10 @@ import { import * as ts from 'typescript'; import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { parseName } from '../utility/parse-name'; +import { validateHtmlSelector, validateName } from '../utility/validation'; import { Schema as ComponentOptions } from './schema'; @@ -42,7 +45,7 @@ function addDeclarationToNgModule(options: ComponentOptions): Rule { const sourceText = text.toString('utf-8'); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); - const componentPath = `/${options.sourceDir}/${options.path}/` + const componentPath = `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + '.component'; @@ -89,10 +92,12 @@ function addDeclarationToNgModule(options: ComponentOptions): Rule { } -function buildSelector(options: ComponentOptions) { +function buildSelector(options: ComponentOptions, projectPrefix: string) { let selector = strings.dasherize(options.name); if (options.prefix) { selector = `${options.prefix}-${selector}`; + } else if (projectPrefix) { + selector = `${projectPrefix}-${selector}`; } return selector; @@ -100,16 +105,27 @@ function buildSelector(options: ComponentOptions) { export default function(options: ComponentOptions): Rule { - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - return (host: Tree, context: SchematicContext) => { - options.selector = options.selector || buildSelector(options); - options.path = options.path ? normalize(options.path) : options.path; + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + options.module = findModuleFromOptions(host, options); + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + options.selector = options.selector || buildSelector(options, project.prefix); + + validateName(options.name); + validateHtmlSelector(options.selector); + const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), options.inlineStyle ? filter(path => !path.endsWith('.__styleext__')) : noop(), @@ -119,7 +135,7 @@ export default function(options: ComponentOptions): Rule { 'if-flat': (s: string) => options.flat ? '' : s, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); return chain([ diff --git a/packages/schematics/angular/component/index_spec.ts b/packages/schematics/angular/component/index_spec.ts index 8e0b65d7bb..b145633db6 100644 --- a/packages/schematics/angular/component/index_spec.ts +++ b/packages/schematics/angular/component/index_spec.ts @@ -5,13 +5,14 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { createAppModule } from '../utility/test'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ComponentOptions } from './schema'; - +// tslint:disable:max-line-length describe('Component Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', @@ -19,8 +20,7 @@ describe('Component Schematic', () => { ); const defaultOptions: ComponentOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', + // path: 'src/app', inlineStyle: false, inlineTemplate: false, changeDetection: 'Default', @@ -28,26 +28,39 @@ describe('Component Schematic', () => { spec: true, module: undefined, export: false, - prefix: 'app', }; - let appTree: Tree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a component', () => { const options = { ...defaultOptions }; - const tree = schematicRunner.runSchematic('component', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.component.css')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.component.html')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.component.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.component.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); + expect(files.indexOf('/projects/bar/src/app/foo/foo.component.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.component.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.component.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.component'/); expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooComponent\r?\n/m); }); @@ -56,7 +69,7 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, changeDetection: 'OnPush' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const tsContent = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(tsContent).toMatch(/changeDetection: ChangeDetectionStrategy.OnPush/); }); @@ -64,7 +77,7 @@ describe('Component Schematic', () => { const options = { ...defaultOptions }; const tree = schematicRunner.runSchematic('component', options, appTree); - const tsContent = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(tsContent).not.toMatch(/encapsulation: ViewEncapsulation/); }); @@ -72,7 +85,7 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, viewEncapsulation: 'Emulated' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const tsContent = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(tsContent).toMatch(/encapsulation: ViewEncapsulation.Emulated/); }); @@ -80,7 +93,7 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, viewEncapsulation: 'None' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const tsContent = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const tsContent = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(tsContent).toMatch(/encapsulation: ViewEncapsulation.None/); }); @@ -89,15 +102,15 @@ describe('Component Schematic', () => { const tree = schematicRunner.runSchematic('component', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo.component.css')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.component.html')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.component.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.component.css')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.component.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.component.ts')).toBeGreaterThanOrEqual(0); }); it('should find the closest module', () => { const options = { ...defaultOptions }; - const fooModule = '/src/app/foo/foo.module.ts'; + const fooModule = '/projects/bar/src/app/foo/foo.module.ts'; appTree.create(fooModule, ` import { NgModule } from '@angular/core'; @@ -109,7 +122,7 @@ describe('Component Schematic', () => { `); const tree = schematicRunner.runSchematic('component', options, appTree); - const fooModuleContent = getFileContent(tree, fooModule); + const fooModuleContent = tree.readContent(fooModule); expect(fooModuleContent).toMatch(/import { FooComponent } from '.\/foo.component'/); }); @@ -117,7 +130,7 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, export: true }; const tree = schematicRunner.runSchematic('component', options, appTree); - const appModuleContent = getFileContent(tree, '/src/app/app.module.ts'); + const appModuleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(appModuleContent).toMatch(/exports: \[FooComponent\]/); }); @@ -125,13 +138,13 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const appModule = getFileContent(tree, '/src/app/app.module.ts'); + const appModule = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(appModule).toMatch(/import { FooComponent } from '.\/foo\/foo.component'/); }); it('should fail if specified module does not exist', () => { - const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; + const options = { ...defaultOptions, module: '/projects/bar/src/app.moduleXXX.ts' }; let thrownError: Error | null = null; try { schematicRunner.runSchematic('component', options, appTree); @@ -142,12 +155,12 @@ describe('Component Schematic', () => { }); it('should handle upper case paths', () => { - const pathOption = 'app/SOME/UPPER/DIR'; + const pathOption = 'projects/bar/src/app/SOME/UPPER/DIR'; const options = { ...defaultOptions, path: pathOption }; const tree = schematicRunner.runSchematic('component', options, appTree); let files = tree.files; - let root = `/src/${pathOption}/foo/foo.component`; + let root = `/${pathOption}/foo/foo.component`; expect(files.indexOf(`${root}.css`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.html`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.spec.ts`)).toBeGreaterThanOrEqual(0); @@ -156,7 +169,7 @@ describe('Component Schematic', () => { const options2 = { ...options, name: 'BAR' }; const tree2 = schematicRunner.runSchematic('component', options2, tree); files = tree2.files; - root = `/src/${pathOption}/bar/bar.component`; + root = `/${pathOption}/bar/bar.component`; expect(files.indexOf(`${root}.css`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.html`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.spec.ts`)).toBeGreaterThanOrEqual(0); @@ -164,11 +177,11 @@ describe('Component Schematic', () => { }); it('should create a component in a sub-directory', () => { - const options = { ...defaultOptions, path: 'app/a/b/c' }; + const options = { ...defaultOptions, path: 'projects/bar/src/app/a/b/c' }; const tree = schematicRunner.runSchematic('component', options, appTree); const files = tree.files; - const root = `/src/${options.path}/foo/foo.component`; + const root = `/${options.path}/foo/foo.component`; expect(files.indexOf(`${root}.css`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.html`)).toBeGreaterThanOrEqual(0); expect(files.indexOf(`${root}.spec.ts`)).toBeGreaterThanOrEqual(0); @@ -179,52 +192,82 @@ describe('Component Schematic', () => { const options = { ...defaultOptions, prefix: 'pre' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const content = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(content).toMatch(/selector: 'pre-foo'/); }); - it('should not use a prefix if none is passed', () => { + it('should use the default project prefix if none is passed', () => { const options = { ...defaultOptions, prefix: undefined }; const tree = schematicRunner.runSchematic('component', options, appTree); - const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); - expect(content).toMatch(/selector: 'foo'/); + const content = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); + expect(content).toMatch(/selector: 'app-foo'/); }); it('should respect the inlineTemplate option', () => { const options = { ...defaultOptions, inlineTemplate: true }; const tree = schematicRunner.runSchematic('component', options, appTree); - const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const content = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(content).toMatch(/template: /); expect(content).not.toMatch(/templateUrl: /); - expect(tree.files.indexOf('/src/app/foo/foo.component.html')).toEqual(-1); + expect(tree.files.indexOf('/projects/bar/src/app/foo/foo.component.html')).toEqual(-1); }); it('should respect the inlineStyle option', () => { const options = { ...defaultOptions, inlineStyle: true }; const tree = schematicRunner.runSchematic('component', options, appTree); - const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const content = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(content).toMatch(/styles: \[/); expect(content).not.toMatch(/styleUrls: /); - expect(tree.files.indexOf('/src/app/foo/foo.component.css')).toEqual(-1); + expect(tree.files.indexOf('/projects/bar/src/app/foo/foo.component.css')).toEqual(-1); }); it('should respect the styleext option', () => { const options = { ...defaultOptions, styleext: 'scss' }; const tree = schematicRunner.runSchematic('component', options, appTree); - const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); + const content = tree.readContent('/projects/bar/src/app/foo/foo.component.ts'); expect(content).toMatch(/styleUrls: \['.\/foo.component.scss/); - expect(tree.files.indexOf('/src/app/foo/foo.component.scss')).toBeGreaterThanOrEqual(0); - expect(tree.files.indexOf('/src/app/foo/foo.component.css')).toEqual(-1); + expect(tree.files.indexOf('/projects/bar/src/app/foo/foo.component.scss')) + .toBeGreaterThanOrEqual(0); + expect(tree.files.indexOf('/projects/bar/src/app/foo/foo.component.css')).toEqual(-1); }); it('should use the module flag even if the module is a routing module', () => { const routingFileName = 'app-routing.module.ts'; - const routingModulePath = `/src/app/${routingFileName}`; + const routingModulePath = `/projects/bar/src/app/${routingFileName}`; const newTree = createAppModule(appTree, routingModulePath); const options = { ...defaultOptions, module: routingFileName }; const tree = schematicRunner.runSchematic('component', options, newTree); - const content = getFileContent(tree, routingModulePath); + const content = tree.readContent(routingModulePath); expect(content).toMatch(/import { FooComponent } from '.\/foo\/foo.component/); }); + + it('should handle a path in the name option', () => { + const options = { ...defaultOptions, name: 'dir/test-component' }; + + const tree = schematicRunner.runSchematic('component', options, appTree); + const content = tree.readContent('/projects/bar/src/app/app.module.ts'); + expect(content).toMatch( + // tslint:disable-next-line:max-line-length + /import { TestComponentComponent } from '\.\/dir\/test-component\/test-component.component'/); + }); + + it('should handle a path in the name and module options', () => { + appTree = schematicRunner.runSchematic('module', { name: 'admin/module' }, appTree); + + const options = { ...defaultOptions, name: 'other/test-component', module: 'admin/module' }; + appTree = schematicRunner.runSchematic('component', options, appTree); + + const content = appTree.readContent('/projects/bar/src/app/admin/module/module.module.ts'); + expect(content).toMatch( + // tslint:disable-next-line:max-line-length + /import { TestComponentComponent } from '..\/..\/other\/test-component\/test-component.component'/); + }); + + it('should create the right selector with a path in the name', () => { + const options = { ...defaultOptions, name: 'sub/test' }; + appTree = schematicRunner.runSchematic('component', options, appTree); + const content = appTree.readContent('/projects/bar/src/app/sub/test/test.component.ts'); + expect(content).toMatch(/selector: 'app-test'/); + }); }); diff --git a/packages/schematics/angular/component/schema.d.ts b/packages/schematics/angular/component/schema.d.ts index 3a410e13ee..81c02e4ab5 100644 --- a/packages/schematics/angular/component/schema.d.ts +++ b/packages/schematics/angular/component/schema.d.ts @@ -12,13 +12,9 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * The name of the component. */ diff --git a/packages/schematics/angular/component/schema.json b/packages/schematics/angular/component/schema.json index 7479112b38..0507039984 100644 --- a/packages/schematics/angular/component/schema.json +++ b/packages/schematics/angular/component/schema.json @@ -8,51 +8,45 @@ "type": "string", "format": "path", "description": "The path to create the component.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "alias": "sd", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "name": { "type": "string", - "description": "The name of the component." + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + } }, "inlineStyle": { "description": "Specifies if the style will be in the ts file.", "type": "boolean", "default": false, - "alias": "is" + "alias": "s" }, "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", "default": false, - "alias": "it" + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", "enum": ["Emulated", "Native", "None"], "type": "string", - "alias": "ve" + "alias": "v" }, "changeDetection": { "description": "Specifies the change detection strategy.", "enum": ["Default", "OnPush"], "type": "string", "default": "Default", - "alias": "cd" + "alias": "c" }, "prefix": { "type": "string", @@ -96,7 +90,5 @@ "description": "Specifies if declaring module exports the component." } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/directive/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts b/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts similarity index 100% rename from packages/schematics/angular/directive/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts rename to packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.spec.ts diff --git a/packages/schematics/angular/directive/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.directive.ts b/packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts similarity index 100% rename from packages/schematics/angular/directive/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.directive.ts rename to packages/schematics/angular/directive/files/__name@dasherize@if-flat__/__name@dasherize__.directive.ts diff --git a/packages/schematics/angular/directive/index.ts b/packages/schematics/angular/directive/index.ts index 2cd937a04b..bcacb61ef4 100644 --- a/packages/schematics/angular/directive/index.ts +++ b/packages/schematics/angular/directive/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -24,7 +24,10 @@ import { import * as ts from 'typescript'; import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { parseName } from '../utility/parse-name'; +import { validateHtmlSelector } from '../utility/validation'; import { Schema as DirectiveOptions } from './schema'; @@ -42,7 +45,7 @@ function addDeclarationToNgModule(options: DirectiveOptions): Rule { const sourceText = text.toString('utf-8'); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); - const directivePath = `/${options.sourceDir}/${options.path}/` + const directivePath = `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + '.directive'; @@ -87,25 +90,38 @@ function addDeclarationToNgModule(options: DirectiveOptions): Rule { } -function buildSelector(options: DirectiveOptions) { +function buildSelector(options: DirectiveOptions, projectPrefix: string) { let selector = options.name; if (options.prefix) { selector = `${options.prefix}-${selector}`; + } else if (projectPrefix) { + selector = `${projectPrefix}-${selector}`; } return strings.camelize(selector); } export default function (options: DirectiveOptions): Rule { - options.selector = options.selector || buildSelector(options); - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + options.module = findModuleFromOptions(host, options); + + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + options.selector = options.selector || buildSelector(options, project.prefix); + + validateHtmlSelector(options.selector); + const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), template({ @@ -113,7 +129,7 @@ export default function (options: DirectiveOptions): Rule { 'if-flat': (s: string) => options.flat ? '' : s, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); return chain([ diff --git a/packages/schematics/angular/directive/index_spec.ts b/packages/schematics/angular/directive/index_spec.ts index 52e33f8f15..9673e3cd2c 100644 --- a/packages/schematics/angular/directive/index_spec.ts +++ b/packages/schematics/angular/directive/index_spec.ts @@ -5,13 +5,13 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as DirectiveOptions } from './schema'; - +// tslint:disable:max-line-length describe('Directive Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', @@ -19,8 +19,6 @@ describe('Directive Schematic', () => { ); const defaultOptions: DirectiveOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', spec: true, module: undefined, export: false, @@ -28,11 +26,25 @@ describe('Directive Schematic', () => { flat: true, }; - let appTree: Tree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a directive', () => { @@ -40,9 +52,9 @@ describe('Directive Schematic', () => { const tree = schematicRunner.runSchematic('directive', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo.directive.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.directive.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); + expect(files.indexOf('/projects/bar/src/app/foo.directive.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.directive.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo.directive'/); expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooDirective\r?\n/m); }); @@ -52,13 +64,13 @@ describe('Directive Schematic', () => { const tree = schematicRunner.runSchematic('directive', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.directive.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.directive.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.directive.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.directive.ts')).toBeGreaterThanOrEqual(0); }); it('should find the closest module', () => { const options = { ...defaultOptions, flat: false }; - const fooModule = '/src/app/foo/foo.module.ts'; + const fooModule = '/projects/bar/src/app/foo/foo.module.ts'; appTree.create(fooModule, ` import { NgModule } from '@angular/core'; @@ -70,7 +82,7 @@ describe('Directive Schematic', () => { `); const tree = schematicRunner.runSchematic('directive', options, appTree); - const fooModuleContent = getFileContent(tree, fooModule); + const fooModuleContent = tree.readContent(fooModule); expect(fooModuleContent).toMatch(/import { FooDirective } from '.\/foo.directive'/); }); @@ -78,7 +90,7 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, export: true }; const tree = schematicRunner.runSchematic('directive', options, appTree); - const appModuleContent = getFileContent(tree, '/src/app/app.module.ts'); + const appModuleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(appModuleContent).toMatch(/exports: \[FooDirective\]/); }); @@ -86,13 +98,13 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('directive', options, appTree); - const appModule = getFileContent(tree, '/src/app/app.module.ts'); + const appModule = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(appModule).toMatch(/import { FooDirective } from '.\/foo.directive'/); }); it('should fail if specified module does not exist', () => { - const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; + const options = { ...defaultOptions, module: '/projects/bar/src/app/app.moduleXXX.ts' }; let thrownError: Error | null = null; try { schematicRunner.runSchematic('directive', options, appTree); @@ -106,7 +118,31 @@ describe('Directive Schematic', () => { const options = { ...defaultOptions, name: 'my-dir' }; const tree = schematicRunner.runSchematic('directive', options, appTree); - const content = getFileContent(tree, '/src/app/my-dir.directive.ts'); + const content = tree.readContent('/projects/bar/src/app/my-dir.directive.ts'); expect(content).toMatch(/selector: '\[appMyDir\]'/); }); + + it('should create the right selector with a path in the name', () => { + const options = { ...defaultOptions, name: 'sub/test' }; + appTree = schematicRunner.runSchematic('directive', options, appTree); + + const content = appTree.readContent('/projects/bar/src/app/sub/test.directive.ts'); + expect(content).toMatch(/selector: '\[appTest\]'/); + }); + + it('should use the prefix', () => { + const options = { ...defaultOptions, prefix: 'pre' }; + const tree = schematicRunner.runSchematic('directive', options, appTree); + + const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + expect(content).toMatch(/selector: '\[preFoo\]'/); + }); + + it('should use the default project prefix if none is passed', () => { + const options = { ...defaultOptions, prefix: undefined }; + const tree = schematicRunner.runSchematic('directive', options, appTree); + + const content = tree.readContent('/projects/bar/src/app/foo.directive.ts'); + expect(content).toMatch(/selector: '\[appFoo\]'/); + }); }); diff --git a/packages/schematics/angular/directive/schema.d.ts b/packages/schematics/angular/directive/schema.d.ts index b7a4e94e75..039c01c749 100644 --- a/packages/schematics/angular/directive/schema.d.ts +++ b/packages/schematics/angular/directive/schema.d.ts @@ -12,21 +12,17 @@ export interface Schema { */ name: string; /** - * The path to create the directive. + * The path to create the interface. */ path?: string; /** - * The root of the application. + * The name of the project. */ - appRoot?: string; + project?: string; /** * The prefix to apply to generated selectors. */ prefix?: string; - /** - * The path of the source directory. - */ - sourceDir?: string; /** * Specifies if a spec file is generated. */ diff --git a/packages/schematics/angular/directive/schema.json b/packages/schematics/angular/directive/schema.json index ce8de2a228..d0f0353313 100644 --- a/packages/schematics/angular/directive/schema.json +++ b/packages/schematics/angular/directive/schema.json @@ -6,19 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the directive." + "description": "The name of the directive.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", - "description": "The path to create the directive.", - "default": "app", + "description": "The path to create the interface.", "visible": false }, - "appRoot": { + "project": { "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "prefix": { @@ -28,13 +30,6 @@ "default": "app", "alias": "p" }, - "sourceDir": { - "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, "spec": { "type": "boolean", "description": "Specifies if a spec file is generated.", @@ -66,7 +61,5 @@ "description": "Specifies if declaring module exports the directive." } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/application/files/protractor.conf.js b/packages/schematics/angular/e2e/files/protractor.conf.js similarity index 86% rename from packages/schematics/angular/application/files/protractor.conf.js rename to packages/schematics/angular/e2e/files/protractor.conf.js index 7ee3b5ee86..86776a391a 100644 --- a/packages/schematics/angular/application/files/protractor.conf.js +++ b/packages/schematics/angular/e2e/files/protractor.conf.js @@ -6,7 +6,7 @@ const { SpecReporter } = require('jasmine-spec-reporter'); exports.config = { allScriptsTimeout: 11000, specs: [ - './e2e/**/*.e2e-spec.ts' + './src/**/*.e2e-spec.ts' ], capabilities: { 'browserName': 'chrome' @@ -21,8 +21,8 @@ exports.config = { }, onPrepare() { require('ts-node').register({ - project: 'e2e/tsconfig.e2e.json' + project: require('path').join(__dirname, './tsconfig.e2e.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } -}; +}; \ No newline at end of file diff --git a/packages/schematics/angular/application/files/e2e/app.e2e-spec.ts b/packages/schematics/angular/e2e/files/src/app.e2e-spec.ts similarity index 60% rename from packages/schematics/angular/application/files/e2e/app.e2e-spec.ts rename to packages/schematics/angular/e2e/files/src/app.e2e-spec.ts index b2c691dc76..e42d1f965f 100644 --- a/packages/schematics/angular/application/files/e2e/app.e2e-spec.ts +++ b/packages/schematics/angular/e2e/files/src/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { AppPage } from './app.po'; -describe('<%= utils.dasherize(name) %> App', () => { +describe('workspace-project App', () => { let page: AppPage; beforeEach(() => { @@ -9,6 +9,6 @@ describe('<%= utils.dasherize(name) %> App', () => { it('should display welcome message', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('Welcome to <%= prefix %>!'); + expect(page.getParagraphText()).toEqual('Welcome to app!'); }); }); diff --git a/packages/schematics/angular/application/files/e2e/app.po.ts b/packages/schematics/angular/e2e/files/src/app.po.ts similarity index 70% rename from packages/schematics/angular/application/files/e2e/app.po.ts rename to packages/schematics/angular/e2e/files/src/app.po.ts index 344e57db79..a3db76a531 100644 --- a/packages/schematics/angular/application/files/e2e/app.po.ts +++ b/packages/schematics/angular/e2e/files/src/app.po.ts @@ -6,6 +6,6 @@ export class AppPage { } getParagraphText() { - return element(by.css('<%= prefix %>-root h1')).getText(); + return element(by.css('<%= rootSelector %> h1')).getText(); } } diff --git a/packages/schematics/angular/e2e/files/tsconfig.e2e.json b/packages/schematics/angular/e2e/files/tsconfig.e2e.json new file mode 100644 index 0000000000..a5e0455e5f --- /dev/null +++ b/packages/schematics/angular/e2e/files/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "<%= appDir.split('/').map(x => '..').join('/') %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= appDir.split('/').map(x => '..').join('/') %>/out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/packages/schematics/angular/e2e/index.ts b/packages/schematics/angular/e2e/index.ts new file mode 100644 index 0000000000..8984b30c28 --- /dev/null +++ b/packages/schematics/angular/e2e/index.ts @@ -0,0 +1,171 @@ +/** + * @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 { strings, tags } from '@angular-devkit/core'; +import { experimental } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + apply, + chain, + mergeWith, + move, + template, + url, +} from '@angular-devkit/schematics'; +import { getWorkspace, getWorkspacePath } from '../utility/config'; +import { Schema as E2eOptions } from './schema'; + +type WorkspaceSchema = experimental.workspace.WorkspaceSchema; + +// TODO: use JsonAST +// function appendPropertyInAstObject( +// recorder: UpdateRecorder, +// node: JsonAstObject, +// propertyName: string, +// value: JsonValue, +// indent = 4, +// ) { +// const indentStr = '\n' + new Array(indent + 1).join(' '); + +// if (node.properties.length > 0) { +// // Insert comma. +// const last = node.properties[node.properties.length - 1]; +// recorder.insertRight(last.start.offset + last.text.replace(/\s+$/, '').length, ','); +// } + +// recorder.insertLeft( +// node.end.offset - 1, +// ' ' +// + `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}` +// + indentStr.slice(0, -2), +// ); +// } + +function addAppToWorkspaceFile(options: E2eOptions, workspace: WorkspaceSchema): Rule { + return (host: Tree, context: SchematicContext) => { + // TODO: use JsonAST + // const workspacePath = '/angular.json'; + // const workspaceBuffer = host.read(workspacePath); + // if (workspaceBuffer === null) { + // throw new SchematicsException(`Configuration file (${workspacePath}) not found.`); + // } + // const workspaceJson = parseJson(workspaceBuffer.toString()); + // if (workspaceJson.value === null) { + // throw new SchematicsException(`Unable to parse configuration file (${workspacePath}).`); + // } + let projectRoot = options.projectRoot !== undefined + ? options.projectRoot + : `${workspace.newProjectRoot}/${options.name}`; + if (projectRoot !== '' && !projectRoot.endsWith('/')) { + projectRoot += '/'; + } + // tslint:disable-next-line:no-any + const project: any = { + root: projectRoot, + projectType: 'application', + architect: { + e2e: { + builder: '@angular-devkit/build-angular:protractor', + options: { + protractorConfig: `${projectRoot}protractor.conf.js`, + devServerTarget: `${options.relatedAppName}:serve`, + }, + }, + lint: { + builder: '@angular-devkit/build-angular:tslint', + options: { + tsConfig: `${projectRoot}tsconfig.e2e.json`, + exclude: [ + '**/node_modules/**', + ], + }, + }, + }, + }; + // tslint:disable-next-line:no-any + // const projects: JsonObject = ( workspaceAst.value).projects || {}; + // tslint:disable-next-line:no-any + // if (!( workspaceAst.value).projects) { + // // tslint:disable-next-line:no-any + // ( workspaceAst.value).projects = projects; + // } + + // TODO: throw if the project already exist. + workspace.projects[options.name] = project; + host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2)); + }; +} +const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/; +const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app']; + +function getRegExpFailPosition(str: string): number | null { + const parts = str.indexOf('-') >= 0 ? str.split('-') : [str]; + const matched: string[] = []; + + parts.forEach(part => { + if (part.match(projectNameRegexp)) { + matched.push(part); + } + }); + + const compare = matched.join('-'); + + return (str !== compare) ? compare.length : null; +} + +function validateProjectName(projectName: string) { + const errorIndex = getRegExpFailPosition(projectName); + if (errorIndex !== null) { + const firstMessage = tags.oneLine` + Project name "${projectName}" is not valid. New project names must + start with a letter, and must contain only alphanumeric characters or dashes. + When adding a dash the segment after the dash must also start with a letter. + `; + const msg = tags.stripIndent` + ${firstMessage} + ${projectName} + ${Array(errorIndex + 1).join(' ') + '^'} + `; + throw new SchematicsException(msg); + } else if (unsupportedProjectNames.indexOf(projectName) !== -1) { + throw new SchematicsException(`Project name "${projectName}" is not a supported name.`); + } + +} + +export default function (options: E2eOptions): Rule { + return (host: Tree, context: SchematicContext) => { + validateProjectName(options.name); + + const workspace = getWorkspace(host); + let newProjectRoot = workspace.newProjectRoot; + let appDir = `${newProjectRoot}/${options.name}`; + + + if (options.projectRoot !== undefined) { + newProjectRoot = options.projectRoot; + appDir = newProjectRoot; + } + + return chain([ + addAppToWorkspaceFile(options, workspace), + mergeWith( + apply(url('./files'), [ + template({ + utils: strings, + ...options, + 'dot': '.', + appDir, + }), + move(appDir), + ])), + ])(host, context); + }; +} diff --git a/packages/schematics/angular/e2e/index_spec.ts b/packages/schematics/angular/e2e/index_spec.ts new file mode 100644 index 0000000000..9d79e29c78 --- /dev/null +++ b/packages/schematics/angular/e2e/index_spec.ts @@ -0,0 +1,102 @@ +/** + * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; +import { Schema as E2eOptions } from './schema'; + +// tslint:disable:max-line-length +describe('Application Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(__dirname, '../collection.json'), + ); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const defaultOptions: E2eOptions = { + name: 'foo', + relatedAppName: 'app', + }; + + let workspaceTree: UnitTestTree; + beforeEach(() => { + workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions); + }); + + it('should create all files of an e2e application', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const files = tree.files; + expect(files.indexOf('/projects/foo/protractor.conf.js')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/tsconfig.e2e.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app.e2e-spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/app.po.ts')).toBeGreaterThanOrEqual(0); + }); + + it('should create all files of an e2e application', () => { + const options = {...defaultOptions, projectRoot: 'e2e'}; + const tree = schematicRunner.runSchematic('e2e', options, workspaceTree); + const files = tree.files; + expect(files.indexOf('/projects/foo/protractor.conf.js')).toEqual(-1); + expect(files.indexOf('/e2e/protractor.conf.js')).toBeGreaterThanOrEqual(0); + }); + + it('should set the rootSelector in the app.po.ts', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const content = tree.readContent('/projects/foo/src/app.po.ts'); + expect(content).toMatch(/app\-root/); + }); + + it('should set the rootSelector in the app.po.ts from the option', () => { + const options = {...defaultOptions, rootSelector: 't-a-c-o'}; + const tree = schematicRunner.runSchematic('e2e', options, workspaceTree); + const content = tree.readContent('/projects/foo/src/app.po.ts'); + expect(content).toMatch(/t\-a\-c\-o/); + }); + + it('should set the rootSelector in the app.po.ts from the option with emoji', () => { + const options = {...defaultOptions, rootSelector: '🌮-🌯'}; + const tree = schematicRunner.runSchematic('e2e', options, workspaceTree); + const content = tree.readContent('/projects/foo/src/app.po.ts'); + expect(content).toMatch(/🌮-🌯/); + }); + + describe('workspace config', () => { + it('should create the e2e app', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo).toBeDefined(); + }); + + it('should set 2 targets for the app', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + const architect = workspace.projects.foo.architect; + expect(Object.keys(architect)).toEqual(['e2e', 'lint']); + }); + + it('should set the e2e options', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + const e2eOptions = workspace.projects.foo.architect.e2e.options; + expect(e2eOptions.protractorConfig).toEqual('projects/foo/protractor.conf.js'); + expect(e2eOptions.devServerTarget).toEqual('app:serve'); + }); + + it('should set the lint options', () => { + const tree = schematicRunner.runSchematic('e2e', defaultOptions, workspaceTree); + const workspace = JSON.parse(tree.readContent('/angular.json')); + const lintOptions = workspace.projects.foo.architect.lint.options; + expect(lintOptions.tsConfig).toEqual('projects/foo/tsconfig.e2e.json'); + }); + }); +}); diff --git a/packages/schematics/angular/e2e/schema.d.ts b/packages/schematics/angular/e2e/schema.d.ts new file mode 100644 index 0000000000..2180f69412 --- /dev/null +++ b/packages/schematics/angular/e2e/schema.d.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The root directory of the new application. + */ + projectRoot?: string; + /** + * The name of the application. + */ + name: string; + /** + * HTML selector for the root component. + */ + rootSelector?: string; + /** + * The name of the app being tested. + */ + relatedAppName: string; +} diff --git a/packages/schematics/angular/e2e/schema.json b/packages/schematics/angular/e2e/schema.json new file mode 100644 index 0000000000..df166cf4a2 --- /dev/null +++ b/packages/schematics/angular/e2e/schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularE2eApp", + "title": "Angular e2e Application Options Schema", + "type": "object", + "properties": { + "projectRoot": { + "description": "The root directory of the new application.", + "type": "string", + "visible": false + }, + "name": { + "description": "The name of the e2e application.", + "type": "string", + "format": "html-selector", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "rootSelector": { + "description": "HTML selector for the root component.", + "type": "string", + "default": "app-root" + }, + "relatedAppName": { + "description": "The name of the app being tested.", + "type": "string" + } + }, + "required": [ + "relatedAppName" + ] +} diff --git a/packages/schematics/angular/enum/files/__path__/__name@dasherize__.enum.ts b/packages/schematics/angular/enum/files/__name@dasherize__.enum.ts similarity index 100% rename from packages/schematics/angular/enum/files/__path__/__name@dasherize__.enum.ts rename to packages/schematics/angular/enum/files/__name@dasherize__.enum.ts diff --git a/packages/schematics/angular/enum/index.ts b/packages/schematics/angular/enum/index.ts index 641205f185..29a40487fb 100644 --- a/packages/schematics/angular/enum/index.ts +++ b/packages/schematics/angular/enum/index.ts @@ -5,10 +5,11 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, - SchematicsException, + SchematicContext, + Tree, apply, branchAndMerge, chain, @@ -17,27 +18,39 @@ import { template, url, } from '@angular-devkit/schematics'; +import { getWorkspace } from '../utility/config'; +import { parseName } from '../utility/parse-name'; import { Schema as EnumOptions } from './schema'; export default function (options: EnumOptions): Rule { - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; - const templateSource = apply(url('./files'), [ - template({ - ...strings, - ...options, - }), - move(sourceDir), - ]); + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } - return chain([ - branchAndMerge(chain([ - mergeWith(templateSource), - ])), - ]); + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + + const templateSource = apply(url('./files'), [ + template({ + ...strings, + ...options, + }), + move(parsedPath.path), + ]); + + return chain([ + branchAndMerge(chain([ + mergeWith(templateSource), + ])), + ])(host, context); + }; } diff --git a/packages/schematics/angular/enum/index_spec.ts b/packages/schematics/angular/enum/index_spec.ts index 0a2a51e362..cf028d9f21 100644 --- a/packages/schematics/angular/enum/index_spec.ts +++ b/packages/schematics/angular/enum/index_spec.ts @@ -5,8 +5,10 @@ * Use 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 { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as EnumOptions } from './schema'; @@ -17,17 +19,38 @@ describe('Enum Schematic', () => { ); const defaultOptions: EnumOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', }; - it('should create an enumeration', () => { - const options = { ...defaultOptions }; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; + beforeEach(() => { + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); + }); - const tree = schematicRunner.runSchematic('enum', options); + it('should create an enumeration', () => { + const tree = schematicRunner.runSchematic('enum', defaultOptions, appTree); const files = tree.files; - expect(files.length).toEqual(1); - expect(files.indexOf('/src/app/foo.enum.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.enum.ts')).toBeGreaterThanOrEqual(0); + }); + it('should create an enumeration', () => { + const tree = schematicRunner.runSchematic('enum', defaultOptions, appTree); + const content = tree.readContent('/projects/bar/src/app/foo.enum.ts'); + expect(content).toMatch('export enum Foo {'); }); diff --git a/packages/schematics/angular/enum/schema.d.ts b/packages/schematics/angular/enum/schema.d.ts index b1176ecf52..47671ee966 100644 --- a/packages/schematics/angular/enum/schema.d.ts +++ b/packages/schematics/angular/enum/schema.d.ts @@ -16,11 +16,7 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; } diff --git a/packages/schematics/angular/enum/schema.json b/packages/schematics/angular/enum/schema.json index c30c95ee5d..a46f149e54 100644 --- a/packages/schematics/angular/enum/schema.json +++ b/packages/schematics/angular/enum/schema.json @@ -6,30 +6,23 @@ "properties": { "name": { "type": "string", - "description": "The name of the enum." + "description": "The name of the enum.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", "description": "The path to create the enum.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/guard/files/__path__/__name@dasherize__.guard.spec.ts b/packages/schematics/angular/guard/files/__name@dasherize__.guard.spec.ts similarity index 100% rename from packages/schematics/angular/guard/files/__path__/__name@dasherize__.guard.spec.ts rename to packages/schematics/angular/guard/files/__name@dasherize__.guard.spec.ts diff --git a/packages/schematics/angular/guard/files/__path__/__name@dasherize__.guard.ts b/packages/schematics/angular/guard/files/__name@dasherize__.guard.ts similarity index 88% rename from packages/schematics/angular/guard/files/__path__/__name@dasherize__.guard.ts rename to packages/schematics/angular/guard/files/__name@dasherize__.guard.ts index 781be9ea3f..3d7117391d 100644 --- a/packages/schematics/angular/guard/files/__path__/__name@dasherize__.guard.ts +++ b/packages/schematics/angular/guard/files/__name@dasherize__.guard.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; @Injectable() export class <%= classify(name) %>Guard implements CanActivate { diff --git a/packages/schematics/angular/guard/index.ts b/packages/schematics/angular/guard/index.ts index e24fafea3e..f57859e5de 100644 --- a/packages/schematics/angular/guard/index.ts +++ b/packages/schematics/angular/guard/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -24,7 +24,9 @@ import { import * as ts from 'typescript'; import { addProviderToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { parseName } from '../utility/parse-name'; import { Schema as GuardOptions } from './schema'; @@ -43,7 +45,7 @@ function addDeclarationToNgModule(options: GuardOptions): Rule { const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); - const guardPath = `/${options.sourceDir}/${options.path}/` + const guardPath = `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + '.guard'; @@ -64,24 +66,32 @@ function addDeclarationToNgModule(options: GuardOptions): Rule { } export default function (options: GuardOptions): Rule { - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + if (options.module) { options.module = findModuleFromOptions(host, options); } + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), template({ ...strings, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); return chain([ diff --git a/packages/schematics/angular/guard/index_spec.ts b/packages/schematics/angular/guard/index_spec.ts index 87bdb5faff..ef5a298759 100644 --- a/packages/schematics/angular/guard/index_spec.ts +++ b/packages/schematics/angular/guard/index_spec.ts @@ -5,10 +5,10 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as GuardOptions } from './schema'; @@ -19,40 +19,49 @@ describe('Guard Schematic', () => { ); const defaultOptions: GuardOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', spec: true, module: undefined, flat: true, }; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; - let appTree: Tree; - + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a guard', () => { - const options = { ...defaultOptions }; - - const tree = schematicRunner.runSchematic('guard', options, appTree); + const tree = schematicRunner.runSchematic('guard', defaultOptions, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo.guard.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.guard.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.guard.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.guard.ts')).toBeGreaterThanOrEqual(0); }); it('should import into a specified module', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('guard', options, appTree); - const appModule = getFileContent(tree, '/src/app/app.module.ts'); + const appModule = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(appModule).toMatch(/import { FooGuard } from '.\/foo.guard'/); }); it('should fail if specified module does not exist', () => { - const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; + const options = { ...defaultOptions, module: '/projects/bar/src/app/app.moduleXXX.ts' }; let thrownError: Error | null = null; try { schematicRunner.runSchematic('guard', options, appTree); @@ -67,15 +76,15 @@ describe('Guard Schematic', () => { const tree = schematicRunner.runSchematic('guard', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo.guard.spec.ts')).toEqual(-1); - expect(files.indexOf('/src/app/foo.guard.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.guard.spec.ts')).toEqual(-1); + expect(files.indexOf('/projects/bar/src/app/foo.guard.ts')).toBeGreaterThanOrEqual(0); }); it('should provide with the module flag', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('guard', options, appTree); - const content = getFileContent(tree, '/src/app/app.module.ts'); + const content = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(content).toMatch(/import.*FooGuard.*from '.\/foo.guard';/); expect(content).toMatch(/providers:\s*\[FooGuard\]/m); }); @@ -84,7 +93,7 @@ describe('Guard Schematic', () => { const options = { ...defaultOptions }; const tree = schematicRunner.runSchematic('guard', options, appTree); - const content = getFileContent(tree, '/src/app/app.module.ts'); + const content = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(content).not.toMatch(/import.*FooGuard.*from '.\/foo.guard';/); expect(content).not.toMatch(/providers:\s*\[FooGuard\]/m); }); diff --git a/packages/schematics/angular/guard/schema.d.ts b/packages/schematics/angular/guard/schema.d.ts index 93ac2aacb3..70ac1d232b 100644 --- a/packages/schematics/angular/guard/schema.d.ts +++ b/packages/schematics/angular/guard/schema.d.ts @@ -24,15 +24,11 @@ export interface Schema { */ module?: string; /** - * The path to create the guard. + * The path to create the interface. */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; } diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index 14b3a441fc..521ec6f629 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -6,7 +6,11 @@ "properties": { "name": { "type": "string", - "description": "The name of the guard." + "description": "The name of the guard.", + "$default": { + "$source": "argv", + "index": 0 + } }, "spec": { "type": "boolean", @@ -26,25 +30,14 @@ "path": { "type": "string", "format": "path", - "description": "The path to create the guard.", - "default": "app", + "description": "The path to create the interface.", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/interface/files/__path__/__name@dasherize____type__.ts b/packages/schematics/angular/interface/files/__name@dasherize____type__.ts similarity index 100% rename from packages/schematics/angular/interface/files/__path__/__name@dasherize____type__.ts rename to packages/schematics/angular/interface/files/__name@dasherize____type__.ts diff --git a/packages/schematics/angular/interface/index.ts b/packages/schematics/angular/interface/index.ts index 189cd913e5..acf04d8d56 100644 --- a/packages/schematics/angular/interface/index.ts +++ b/packages/schematics/angular/interface/index.ts @@ -5,10 +5,11 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, - SchematicsException, + SchematicContext, + Tree, apply, branchAndMerge, chain, @@ -17,29 +18,42 @@ import { template, url, } from '@angular-devkit/schematics'; +import { getWorkspace } from '../utility/config'; +import { parseName } from '../utility/parse-name'; import { Schema as InterfaceOptions } from './schema'; export default function (options: InterfaceOptions): Rule { - options.prefix = options.prefix ? options.prefix : ''; - options.type = !!options.type ? `.${options.type}` : ''; - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; - const templateSource = apply(url('./files'), [ - template({ - ...strings, - ...options, - }), - move(sourceDir), - ]); + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } - return chain([ - branchAndMerge(chain([ - mergeWith(templateSource), - ])), - ]); + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + + options.prefix = options.prefix ? options.prefix : ''; + options.type = !!options.type ? `.${options.type}` : ''; + + const templateSource = apply(url('./files'), [ + template({ + ...strings, + ...options, + }), + move(parsedPath.path), + ]); + + return chain([ + branchAndMerge(chain([ + mergeWith(templateSource), + ])), + ])(host, context); + }; } diff --git a/packages/schematics/angular/interface/index_spec.ts b/packages/schematics/angular/interface/index_spec.ts index 16d342402c..2f9d8232ba 100644 --- a/packages/schematics/angular/interface/index_spec.ts +++ b/packages/schematics/angular/interface/index_spec.ts @@ -5,9 +5,10 @@ * Use 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 { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as InterfaceOptions } from './schema'; @@ -18,29 +19,47 @@ describe('Interface Schematic', () => { ); const defaultOptions: InterfaceOptions = { name: 'foo', - path: 'app', prefix: '', - sourceDir: 'src', type: '', }; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; + beforeEach(() => { + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); + }); + it('should create one file', () => { - const tree = schematicRunner.runSchematic('interface', defaultOptions); - expect(tree.files.length).toEqual(1); - expect(tree.files[0]).toEqual('/src/app/foo.ts'); + const tree = schematicRunner.runSchematic('interface', defaultOptions, appTree); + expect(tree.files.indexOf('/projects/bar/src/app/foo.ts')).toBeGreaterThanOrEqual(0); }); it('should create an interface named "Foo"', () => { - const tree = schematicRunner.runSchematic('interface', defaultOptions); - const fileContent = getFileContent(tree, '/src/app/foo.ts'); + const tree = schematicRunner.runSchematic('interface', defaultOptions, appTree); + const fileContent = tree.readContent('/projects/bar/src/app/foo.ts'); expect(fileContent).toMatch(/export interface Foo/); }); it('should put type in the file name', () => { const options = { ...defaultOptions, type: 'model' }; - const tree = schematicRunner.runSchematic('interface', options); - expect(tree.files[0]).toEqual('/src/app/foo.model.ts'); + const tree = schematicRunner.runSchematic('interface', options, appTree); + expect(tree.files.indexOf('/projects/bar/src/app/foo.model.ts')).toBeGreaterThanOrEqual(0); }); }); diff --git a/packages/schematics/angular/interface/schema.d.ts b/packages/schematics/angular/interface/schema.d.ts index 92f220ab94..f38ea5158c 100644 --- a/packages/schematics/angular/interface/schema.d.ts +++ b/packages/schematics/angular/interface/schema.d.ts @@ -16,13 +16,9 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * Specifies the prefix to use. */ diff --git a/packages/schematics/angular/interface/schema.json b/packages/schematics/angular/interface/schema.json index 3403e12d23..a13f65c1da 100644 --- a/packages/schematics/angular/interface/schema.json +++ b/packages/schematics/angular/interface/schema.json @@ -6,26 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the interface." + "description": "The name of the interface.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", "description": "The path to create the interface.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "prefix": { @@ -36,10 +31,11 @@ "type": { "type": "string", "description": "Specifies the type of interface.", - "default": "" + "$default": { + "$source": "argv", + "index": 1 + } } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/library/files/__projectRoot__/karma.conf.js b/packages/schematics/angular/library/files/__projectRoot__/karma.conf.js new file mode 100644 index 0000000000..d5fd42ec88 --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/karma.conf.js @@ -0,0 +1,34 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, 'coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + angularCli: { + environment: 'dev' + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/packages/schematics/angular/library/files/__projectRoot__/ng-package.json b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json new file mode 100644 index 0000000000..123cc3811b --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json @@ -0,0 +1,8 @@ +{ + "$schema": "<%= projectRoot.split('/').map(x => '..').join('/') %>/node_modules/ng-packagr/ng-package.schema.json", + "dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/<%= dasherize(name) %>", + "deleteDestPath": false, + "lib": { + "entryFile": "src/<%= entryFile %>.ts" + } +} \ No newline at end of file diff --git a/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json b/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json new file mode 100644 index 0000000000..f41a75a4cc --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/ng-package.prod.json @@ -0,0 +1,7 @@ +{ + "$schema": "<%= projectRoot.split('/').map(x => '..').join('/') %>/node_modules/ng-packagr/ng-package.schema.json", + "dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/lib", + "lib": { + "entryFile": "src/<%= entryFile %>.ts" + } +} \ No newline at end of file diff --git a/packages/schematics/angular/library/files/__projectRoot__/package.json b/packages/schematics/angular/library/files/__projectRoot__/package.json new file mode 100644 index 0000000000..66125d35f0 --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/package.json @@ -0,0 +1,8 @@ +{ + "name": "<%= dasherize(name) %>", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^6.0.0-rc.0 || ^6.0.0", + "@angular/core": "^6.0.0-rc.0 || ^6.0.0" + } +} \ No newline at end of file diff --git a/packages/schematics/angular/library/files/__projectRoot__/src/__entryFile__.ts b/packages/schematics/angular/library/files/__projectRoot__/src/__entryFile__.ts new file mode 100644 index 0000000000..b4d46cdc03 --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/src/__entryFile__.ts @@ -0,0 +1,7 @@ +/* + * Public API Surface of <%= dasherize(name) %> + */ + +export * from './lib/<%= dasherize(name) %>.service'; +export * from './lib/<%= dasherize(name) %>.component'; +export * from './lib/<%= dasherize(name) %>.module'; diff --git a/packages/schematics/angular/library/files/__projectRoot__/src/test.ts b/packages/schematics/angular/library/files/__projectRoot__/src/test.ts new file mode 100644 index 0000000000..e11ff1c97b --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/src/test.ts @@ -0,0 +1,22 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'core-js/es7/reflect'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/packages/schematics/angular/library/files/__projectRoot__/tsconfig.lint.json b/packages/schematics/angular/library/files/__projectRoot__/tsconfig.lint.json new file mode 100644 index 0000000000..2a80c8ebc4 --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/tsconfig.lint.json @@ -0,0 +1,12 @@ +{ + "extends": "<%= projectRoot.split('/').map(x => '..').join('/') %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= projectRoot.split('/').map(x => '..').join('/') %>/out-tsc/spec", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/packages/schematics/angular/library/files/__projectRoot__/tsconfig.spec.json b/packages/schematics/angular/library/files/__projectRoot__/tsconfig.spec.json new file mode 100644 index 0000000000..0ebff0e1ac --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "<%= projectRoot.split('/').map(x => '..').join('/') %>/tsconfig.json", + "compilerOptions": { + "outDir": "<%= projectRoot.split('/').map(x => '..').join('/') %>/out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/packages/schematics/angular/library/files/__projectRoot__/tslint.json b/packages/schematics/angular/library/files/__projectRoot__/tslint.json new file mode 100644 index 0000000000..2eec2dd9cc --- /dev/null +++ b/packages/schematics/angular/library/files/__projectRoot__/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "<%= relativeTsLintPath %>/tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "<%= prefix %>", + "camelCase" + ], + "component-selector": [ + true, + "element", + "<%= prefix %>", + "kebab-case" + ] + } +} diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts new file mode 100644 index 0000000000..93c69c6fb7 --- /dev/null +++ b/packages/schematics/angular/library/index.ts @@ -0,0 +1,229 @@ +/** + * @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 { strings } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + apply, + branchAndMerge, + chain, + mergeWith, + noop, + schematic, + template, + url, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { WorkspaceSchema, getWorkspace, getWorkspacePath } from '../utility/config'; +import { latestVersions } from '../utility/latest-versions'; +import { Schema as LibraryOptions } from './schema'; + + +type PackageJsonPartialType = { + scripts: { + [key: string]: string; + }, + dependencies: { + [key: string]: string; + }, + devDependencies: { + [key: string]: string; + }, +}; + +interface UpdateJsonFn { + (obj: T): T | void; +} + +type TsConfigPartialType = { + compilerOptions: { + baseUrl: string, + paths: { + [key: string]: string[]; + }, + }, +}; + +function updateJsonFile(host: Tree, path: string, callback: UpdateJsonFn): Tree { + const source = host.read(path); + if (source) { + const sourceText = source.toString('utf-8'); + const json = JSON.parse(sourceText); + callback(json); + host.overwrite(path, JSON.stringify(json, null, 2)); + } + + return host; +} + +function updateTsConfig(npmPackageName: string) { + + return (host: Tree) => { + if (!host.exists('tsconfig.json')) { return host; } + + return updateJsonFile(host, 'tsconfig.json', (tsconfig: TsConfigPartialType) => { + if (!tsconfig.compilerOptions.paths) { + tsconfig.compilerOptions.paths = {}; + } + if (!tsconfig.compilerOptions.paths[npmPackageName]) { + tsconfig.compilerOptions.paths[npmPackageName] = []; + } + tsconfig.compilerOptions.paths[npmPackageName].push(`dist/${npmPackageName}`); + }); + }; +} + +function addDependenciesToPackageJson() { + + return (host: Tree) => { + if (!host.exists('package.json')) { return host; } + + return updateJsonFile(host, 'package.json', (json: PackageJsonPartialType) => { + + + if (!json['dependencies']) { + json['dependencies'] = {}; + } + + json.dependencies = { + '@angular/common': latestVersions.Angular, + '@angular/core': latestVersions.Angular, + '@angular/compiler': latestVersions.Angular, + // De-structure last keeps existing user dependencies. + ...json.dependencies, + }; + + if (!json['devDependencies']) { + json['devDependencies'] = {}; + } + + json.devDependencies = { + '@angular/compiler-cli': latestVersions.Angular, + '@angular-devkit/build-ng-packagr': latestVersions.DevkitBuildNgPackagr, + '@angular-devkit/build-angular': latestVersions.DevkitBuildNgPackagr, + 'ng-packagr': '^2.4.1', + 'tsickle': '>=0.25.5', + 'tslib': '^1.7.1', + 'typescript': latestVersions.TypeScript, + // De-structure last keeps existing user dependencies. + ...json.devDependencies, + }; + }); + }; +} + +function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSchema, + projectRoot: string): Rule { + return (host: Tree, context: SchematicContext) => { + + // tslint:disable-next-line:no-any + const project: any = { + root: `${projectRoot}`, + projectType: 'library', + prefix: options.prefix || 'lib', + architect: { + build: { + builder: '@angular-devkit/build-ng-packagr:build', + options: { + project: `${projectRoot}/ng-package.json`, + }, + configurations: { + production: { + project: `${projectRoot}/ng-package.prod.json`, + }, + }, + }, + test: { + builder: '@angular-devkit/build-angular:karma', + options: { + main: `${projectRoot}/src/test.ts`, + tsConfig: `${projectRoot}/tsconfig.spec.json`, + karmaConfig: `${projectRoot}/karma.conf.js`, + }, + }, + lint: { + builder: '@angular-devkit/build-angular:tslint', + options: { + tsConfig: [ + `${projectRoot}/tsconfig.lint.json`, + `${projectRoot}/tsconfig.spec.json`, + ], + exclude: [ + '**/node_modules/**', + ], + }, + }, + }, + }; + + workspace.projects[options.name] = project; + host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2)); + }; +} + +export default function (options: LibraryOptions): Rule { + return (host: Tree, context: SchematicContext) => { + if (!options.name) { + throw new SchematicsException(`Invalid options, "name" is required.`); + } + const name = options.name; + const prefix = options.prefix || 'lib'; + + const workspace = getWorkspace(host); + const newProjectRoot = workspace.newProjectRoot; + const projectRoot = `${newProjectRoot}/${strings.dasherize(options.name)}`; + const sourceDir = `${projectRoot}/src/lib`; + const relativeTsLintPath = projectRoot.split('/').map(x => '..').join('/'); + + const templateSource = apply(url('./files'), [ + template({ + ...strings, + ...options, + projectRoot, + relativeTsLintPath, + prefix, + }), + // TODO: Moving inside `branchAndMerge` should work but is bugged right now. + // The __projectRoot__ is being used meanwhile. + // move(projectRoot), + ]); + + return chain([ + branchAndMerge(mergeWith(templateSource)), + addAppToWorkspaceFile(options, workspace, projectRoot), + options.skipPackageJson ? noop() : addDependenciesToPackageJson(), + options.skipTsConfig ? noop() : updateTsConfig(name), + schematic('module', { + name: name, + commonModule: false, + flat: true, + path: sourceDir, + spec: false, + }), + schematic('component', { + name: name, + selector: `${prefix}-${name}`, + inlineStyle: true, + inlineTemplate: true, + flat: true, + path: sourceDir, + export: true, + }), + schematic('service', { + name: name, + flat: true, + path: sourceDir, + }), + (_tree: Tree, context: SchematicContext) => { + context.addTask(new NodePackageInstallTask()); + }, + ])(host, context); + }; +} diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts new file mode 100644 index 0000000000..47a76f0c6c --- /dev/null +++ b/packages/schematics/angular/library/index_spec.ts @@ -0,0 +1,206 @@ +/** + * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { getFileContent } from '../../angular/utility/test'; +import { latestVersions } from '../utility/latest-versions'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; +import { Schema as GenerateLibrarySchema } from './schema'; + +function getJsonFileContent(tree: UnitTestTree, path: string) { + return JSON.parse(tree.readContent(path)); +} + +// tslint:disable:max-line-length +describe('Library Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/ng_packagr', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: GenerateLibrarySchema = { + name: 'foo', + entryFile: 'my_index', + skipPackageJson: false, + skipTsConfig: false, + }; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + let workspaceTree: UnitTestTree; + beforeEach(() => { + workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions); + }); + + it('should create files', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const files = tree.files; + expect(files.indexOf('/projects/foo/karma.conf.js')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/ng-package.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/package.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/tslint.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/test.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/my_index.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/lib/foo.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/lib/foo.component.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/lib/foo.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/lib/foo.service.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/foo/src/lib/foo.service.ts')).toBeGreaterThanOrEqual(0); + }); + + it('should create a package.json named "foo"', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const fileContent = getFileContent(tree, '/projects/foo/package.json'); + expect(fileContent).toMatch(/"name": "foo"/); + }); + + it('should create a ng-package.json with ngPackage conf', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const fileContent = getJsonFileContent(tree, '/projects/foo/ng-package.json'); + expect(fileContent.lib).toBeDefined(); + expect(fileContent.lib.entryFile).toEqual('src/my_index.ts'); + expect(fileContent.deleteDestPath).toEqual(false); + expect(fileContent.dest).toEqual('../../dist/foo'); + }); + + it('should use default value for baseDir and entryFile', () => { + const tree = schematicRunner.runSchematic('library', { + name: 'foobar', + }, workspaceTree); + expect(tree.files.indexOf('/projects/foobar/src/public_api.ts')).toBeGreaterThanOrEqual(0); + }); + + it(`should add library workspace`, () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + + const workspace = getJsonFileContent(tree, '/angular.json'); + expect(workspace.projects.foo).toBeDefined(); + }); + + it('should set the prefix to lib if none is set', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo.prefix).toEqual('lib'); + }); + + it('should set the prefix correctly', () => { + const options = { ...defaultOptions, prefix: 'pre' }; + const tree = schematicRunner.runSchematic('application', options, workspaceTree); + + const workspace = JSON.parse(tree.readContent('/angular.json')); + expect(workspace.projects.foo.prefix).toEqual('pre'); + }); + + it('should handle a pascalCasedName', () => { + const options = {...defaultOptions, name: 'pascalCasedName'}; + const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const config = getJsonFileContent(tree, '/angular.json'); + const project = config.projects.pascalCasedName; + expect(project).toBeDefined(); + expect(project.root).toEqual('projects/pascal-cased-name'); + const svcContent = tree.readContent('/projects/pascal-cased-name/src/lib/pascal-cased-name.service.ts'); + expect(svcContent).toMatch(/providedIn: 'root'/); + }); + + it('should export the component in the NgModule', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const fileContent = getFileContent(tree, '/projects/foo/src/lib/foo.module.ts'); + expect(fileContent).toContain('exports: [FooComponent]'); + }); + + it('should set the right path and prefix in the tslint file', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const path = '/projects/foo/tslint.json'; + const content = JSON.parse(tree.readContent(path)); + expect(content.extends).toMatch('../../tslint.json'); + expect(content.rules['directive-selector'][2]).toMatch('lib'); + expect(content.rules['component-selector'][2]).toMatch('lib'); + }); + + describe(`update package.json`, () => { + it(`should add ng-packagr to devDependencies`, () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + + const packageJson = getJsonFileContent(tree, 'package.json'); + expect(packageJson.devDependencies['ng-packagr']).toEqual('^2.4.1'); + expect(packageJson.devDependencies['@angular-devkit/build-ng-packagr']) + .toEqual(latestVersions.DevkitBuildNgPackagr); + }); + + it('should use the latest known versions in package.json', () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@angular/compiler-cli']).toEqual(latestVersions.Angular); + expect(pkg.devDependencies['typescript']).toEqual(latestVersions.TypeScript); + }); + + it(`should not override existing users dependencies`, () => { + const oldPackageJson = workspaceTree.readContent('package.json'); + workspaceTree.overwrite('package.json', oldPackageJson.replace( + `"typescript": "${latestVersions.TypeScript}"`, + `"typescript": "~2.5.2"`, + )); + + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const packageJson = getJsonFileContent(tree, 'package.json'); + expect(packageJson.devDependencies.typescript).toEqual('~2.5.2'); + }); + + it(`should not modify the file when --skipPackageJson`, () => { + const tree = schematicRunner.runSchematic('library', { + name: 'foo', + skipPackageJson: true, + }, workspaceTree); + + const packageJson = getJsonFileContent(tree, 'package.json'); + expect(packageJson.devDependencies['ng-packagr']).toBeUndefined(); + expect(packageJson.devDependencies['@angular-devkit/build-angular']).toBeUndefined(); + }); + }); + + describe(`update tsconfig.json`, () => { + it(`should add paths mapping to empty tsconfig`, () => { + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths.foo).toBeTruthy(); + expect(tsConfigJson.compilerOptions.paths.foo.length).toEqual(1); + expect(tsConfigJson.compilerOptions.paths.foo[0]).toEqual('dist/foo'); + }); + + it(`should append to existing paths mappings`, () => { + workspaceTree.overwrite('tsconfig.json', JSON.stringify({ + compilerOptions: { + paths: { + 'unrelated': ['./something/else.ts'], + 'foo': ['libs/*'], + }, + }, + })); + const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths.foo).toBeTruthy(); + expect(tsConfigJson.compilerOptions.paths.foo.length).toEqual(2); + expect(tsConfigJson.compilerOptions.paths.foo[1]).toEqual('dist/foo'); + }); + + it(`should not modify the file when --skipTsConfig`, () => { + const tree = schematicRunner.runSchematic('library', { + name: 'foo', + skipTsConfig: true, + }, workspaceTree); + + const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); + expect(tsConfigJson.compilerOptions.paths).toBeUndefined(); + }); + }); +}); diff --git a/packages/schematics/angular/library/schema.d.ts b/packages/schematics/angular/library/schema.d.ts new file mode 100644 index 0000000000..d833e503d5 --- /dev/null +++ b/packages/schematics/angular/library/schema.d.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The name of the library. Required field. + */ + name: string; + /** + * The path to create the interface. + */ + entryFile: string; + /** + * The prefix to apply to generated selectors. + */ + prefix?: string; + /** + * Do not add dependencies to package.json (e.g., --skipPackageJson) + */ + skipPackageJson: boolean; + /** + * Do not update tsconfig.json for development experience (e.g., --skipTsConfig) + */ + skipTsConfig: boolean; +} diff --git a/packages/schematics/angular/library/schema.json b/packages/schematics/angular/library/schema.json new file mode 100644 index 0000000000..9a3873f614 --- /dev/null +++ b/packages/schematics/angular/library/schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsLibrary", + "title": "Library Options Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the library.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "entryFile": { + "type": "string", + "format": "path", + "description": "The path to create the library's public API file.", + "default": "public_api" + }, + "prefix": { + "type": "string", + "format": "html-selector", + "description": "The prefix to apply to generated selectors.", + "default": "lib", + "alias": "p" + }, + "skipPackageJson": { + "type": "boolean", + "default": false, + "description": "Do not add dependencies to package.json." + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update tsconfig.json for development experience." + } + }, + "required": [] +} diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json new file mode 100644 index 0000000000..eaf1c49670 --- /dev/null +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -0,0 +1,9 @@ +{ + "schematics": { + "migration-01": { + "version": "6.0.0-beta.8", + "factory": "./update-6", + "description": "Update an Angular CLI project to version 6." + } + } +} \ No newline at end of file diff --git a/packages/schematics/angular/migrations/update-6/index.ts b/packages/schematics/angular/migrations/update-6/index.ts new file mode 100644 index 0000000000..a2c1d84dee --- /dev/null +++ b/packages/schematics/angular/migrations/update-6/index.ts @@ -0,0 +1,607 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject, Path, join, normalize } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { AppConfig, CliConfig } from '../../utility/config'; +import { latestVersions } from '../../utility/latest-versions'; + +const defaults = { + appRoot: 'src', + index: 'index.html', + main: 'main.ts', + polyfills: 'polyfills.ts', + tsConfig: 'tsconfig.app.json', + test: 'test.ts', + outDir: 'dist/', + karma: 'karma.conf.js', + protractor: 'protractor.conf.js', + testTsConfig: 'tsconfig.spec.json', + serverOutDir: 'dist-server', + serverMain: 'main.server.ts', + serverTsConfig: 'tsconfig.server.json', +}; + +function getConfigPath(tree: Tree): Path { + let possiblePath = normalize('.angular-cli.json'); + if (tree.exists(possiblePath)) { + return possiblePath; + } + possiblePath = normalize('angular-cli.json'); + if (tree.exists(possiblePath)) { + return possiblePath; + } + + throw new SchematicsException('Could not find configuration file'); +} + +function migrateKarmaConfiguration(config: CliConfig): Rule { + return (host: Tree, context: SchematicContext) => { + context.logger.info(`Updating karma configuration`); + try { + const karmaPath = config && config.test && config.test.karma && config.test.karma.config + ? config.test.karma.config + : defaults.karma; + const buffer = host.read(karmaPath); + if (buffer !== null) { + let content = buffer.toString(); + content = content.replace( /@angular\/cli/g, '@angular-devkit/build-angular'); + content = content.replace('reports', + `dir: require('path').join(__dirname, 'coverage'), reports`); + host.overwrite(karmaPath, content); + } + } catch (e) { } + + return host; + }; +} + +function migrateConfiguration(oldConfig: CliConfig): Rule { + return (host: Tree, context: SchematicContext) => { + const oldConfigPath = getConfigPath(host); + const configPath = normalize('angular.json'); + context.logger.info(`Updating configuration`); + const config: JsonObject = { + '$schema': './node_modules/@angular-devkit/core/src/workspace/workspace-schema.json', + version: 1, + newProjectRoot: 'projects', + projects: extractProjectsConfig(oldConfig, host), + }; + const cliConfig = extractCliConfig(oldConfig); + if (cliConfig !== null) { + config.cli = cliConfig; + } + const schematicsConfig = extractSchematicsConfig(oldConfig); + if (schematicsConfig !== null) { + config.schematics = schematicsConfig; + } + const architectConfig = extractArchitectConfig(oldConfig); + if (architectConfig !== null) { + config.architect = architectConfig; + } + + context.logger.info(`Removing old config file (${oldConfigPath})`); + host.delete(oldConfigPath); + context.logger.info(`Writing config file (${configPath})`); + host.create(configPath, JSON.stringify(config, null, 2)); + + return host; + }; +} + +function extractCliConfig(config: CliConfig): JsonObject | null { + const newConfig: JsonObject = {}; + if (config.packageManager && config.packageManager !== 'default') { + newConfig['packageManager'] = config.packageManager; + } + + return newConfig; +} + +function extractSchematicsConfig(config: CliConfig): JsonObject | null { + let collectionName = '@schematics/angular'; + if (!config || !config.defaults) { + return null; + } + // const configDefaults = config.defaults; + if (config.defaults && config.defaults.schematics && config.defaults.schematics.collection) { + collectionName = config.defaults.schematics.collection; + } + + /** + * For each schematic + * - get the config + * - filter one's without config + * - combine them into an object + */ + // tslint:disable-next-line:no-any + const schematicConfigs: any = ['class', 'component', 'directive', 'guard', + 'interface', 'module', 'pipe', 'service'] + .map(schematicName => { + // tslint:disable-next-line:no-any + const schematicDefaults: JsonObject = (config.defaults as any)[schematicName] || null; + + return { + schematicName, + config: schematicDefaults, + }; + }) + .filter(schematic => schematic.config !== null) + .reduce((all: JsonObject, schematic) => { + all[collectionName + ':' + schematic.schematicName] = schematic.config; + + return all; + }, {}); + + const componentUpdate: JsonObject = {}; + componentUpdate.prefix = ''; + + const componentKey = collectionName + ':component'; + const directiveKey = collectionName + ':directive'; + if (!schematicConfigs[componentKey]) { + schematicConfigs[componentKey] = {}; + } + if (!schematicConfigs[directiveKey]) { + schematicConfigs[directiveKey] = {}; + } + if (config.apps && config.apps[0]) { + schematicConfigs[componentKey].prefix = config.apps[0].prefix; + schematicConfigs[directiveKey].prefix = config.apps[0].prefix; + } + if (config.defaults) { + schematicConfigs[componentKey].styleext = config.defaults.styleExt; + } + + return schematicConfigs; +} + +function extractArchitectConfig(_config: CliConfig): JsonObject | null { + return null; +} + +function extractProjectsConfig(config: CliConfig, tree: Tree): JsonObject { + const builderPackage = '@angular-devkit/build-angular'; + let defaultAppNamePrefix = 'app'; + if (config.project && config.project.name) { + defaultAppNamePrefix = config.project.name; + } + + const buildDefaults: JsonObject = config.defaults && config.defaults.build + ? { + sourceMap: config.defaults.build.sourcemaps, + progress: config.defaults.build.progress, + poll: config.defaults.build.poll, + deleteOutputPath: config.defaults.build.deleteOutputPath, + preserveSymlinks: config.defaults.build.preserveSymlinks, + showCircularDependencies: config.defaults.build.showCircularDependencies, + commonChunk: config.defaults.build.commonChunk, + namedChunks: config.defaults.build.namedChunks, + } as JsonObject + : {}; + + const serveDefaults: JsonObject = config.defaults && config.defaults.serve + ? { + port: config.defaults.serve.port, + host: config.defaults.serve.host, + ssl: config.defaults.serve.ssl, + sslKey: config.defaults.serve.sslKey, + sslCert: config.defaults.serve.sslCert, + proxyConfig: config.defaults.serve.proxyConfig, + } as JsonObject + : {}; + + + const apps = config.apps || []; + // convert the apps to projects + const browserApps = apps.filter(app => app.platform !== 'server'); + const serverApps = apps.filter(app => app.platform === 'server'); + + const projectMap = browserApps + .map((app, idx) => { + const defaultAppName = idx === 0 ? defaultAppNamePrefix : `${defaultAppNamePrefix}${idx}`; + const name = app.name || defaultAppName; + const outDir = app.outDir || defaults.outDir; + const appRoot = app.root || defaults.appRoot; + + function _mapAssets(asset: string | JsonObject) { + if (typeof asset === 'string') { + if (tree.exists(app.root + '/' + asset)) { + // If it exists in the tree, then it is a file. + return { glob: asset, input: normalize(appRoot + '/'), output: '/' }; + } else { + // If it does not exist, it is either a folder or something we can't statically know. + // Folders must get a recursive star glob. + return { glob: '**/*', input: normalize(appRoot + '/' + asset), output: '/' + asset }; + } + } else { + if (asset.output) { + return { + glob: asset.glob, + input: normalize(appRoot + '/' + asset.input), + output: normalize('/' + asset.output as string), + }; + } else { + return { + glob: asset.glob, + input: normalize(appRoot + '/' + asset.input), + output: '/', + }; + } + } + } + + function _buildConfigurations(): JsonObject { + const source = app.environmentSource; + const environments = app.environments; + const serviceWorker = app.serviceWorker; + + if (!environments) { + return {}; + } + + return Object.keys(environments).reduce((acc, environment) => { + if (source === environments[environment]) { + return acc; + } + + let isProduction = false; + + const environmentContent = tree.read(app.root + '/' + environments[environment]); + if (environmentContent) { + isProduction = !!environmentContent.toString('utf-8') + // Allow for `production: true` or `production = true`. Best we can do to guess. + .match(/production['"]?\s*[:=]\s*true/); + } + + let configurationName; + // We used to use `prod` by default as the key, instead we now use the full word. + // Try not to override the production key if it's there. + if (environment == 'prod' && !environments['production'] && isProduction) { + configurationName = 'production'; + } else { + configurationName = environment; + } + + acc[configurationName] = { + ...(isProduction + ? { + optimization: true, + outputHashing: 'all', + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + } + : {} + ), + ...(isProduction && serviceWorker ? { serviceWorker: true } : {}), + fileReplacements: [ + { + replace: `${app.root}/${source}`, + with: `${app.root}/${environments[environment]}`, + }, + ], + }; + + return acc; + }, {} as JsonObject); + } + + function _serveConfigurations(): JsonObject { + const environments = app.environments; + + if (!environments) { + return {}; + } + if (!architect) { + throw new Error(); + } + + const configurations = (architect.build as JsonObject).configurations as JsonObject; + + return Object.keys(configurations).reduce((acc, environment) => { + acc[environment] = { browserTarget: `${name}:build:${environment}` }; + + return acc; + }, {} as JsonObject); + } + + function _extraEntryMapper(extraEntry: string | JsonObject) { + let entry: string | JsonObject; + if (typeof extraEntry === 'string') { + entry = join(app.root as Path, extraEntry); + } else { + const input = join(app.root as Path, extraEntry.input as string || ''); + entry = { input, lazy: extraEntry.lazy }; + + if (extraEntry.output) { + entry.bundleName = extraEntry.output; + } + } + + return entry; + } + + const project: JsonObject = { + root: '', + projectType: 'application', + }; + + const architect: JsonObject = {}; + project.architect = architect; + + // Browser target + const buildOptions: JsonObject = { + // Make outputPath relative to root. + outputPath: outDir, + index: `${appRoot}/${app.index || defaults.index}`, + main: `${appRoot}/${app.main || defaults.main}`, + tsConfig: `${appRoot}/${app.tsconfig || defaults.tsConfig}`, + ...buildDefaults, + }; + + if (app.polyfills) { + buildOptions.polyfills = appRoot + '/' + app.polyfills; + } + + buildOptions.assets = (app.assets || []).map(_mapAssets); + buildOptions.styles = (app.styles || []).map(_extraEntryMapper); + buildOptions.scripts = (app.scripts || []).map(_extraEntryMapper); + architect.build = { + builder: `${builderPackage}:browser`, + options: buildOptions, + configurations: _buildConfigurations(), + }; + + // Serve target + const serveOptions: JsonObject = { + browserTarget: `${name}:build`, + ...serveDefaults, + }; + architect.serve = { + builder: `${builderPackage}:dev-server`, + options: serveOptions, + configurations: _serveConfigurations(), + }; + + // Extract target + const extractI18nOptions: JsonObject = { browserTarget: `${name}:build` }; + architect['extract-i18n'] = { + builder: `${builderPackage}:extract-i18n`, + options: extractI18nOptions, + }; + + const karmaConfig = config.test && config.test.karma + ? config.test.karma.config || '' + : ''; + // Test target + const testOptions: JsonObject = { + main: appRoot + '/' + app.test || defaults.test, + // Make karmaConfig relative to root. + karmaConfig, + }; + + if (app.polyfills) { + testOptions.polyfills = appRoot + '/' + app.polyfills; + } + + if (app.testTsconfig) { + testOptions.tsConfig = appRoot + '/' + app.testTsconfig; + } + testOptions.scripts = (app.scripts || []).map(_extraEntryMapper); + testOptions.styles = (app.styles || []).map(_extraEntryMapper); + testOptions.assets = (app.assets || []).map(_mapAssets); + + if (karmaConfig) { + architect.test = { + builder: `${builderPackage}:karma`, + options: testOptions, + }; + } + + const tsConfigs: string[] = []; + const excludes: string[] = []; + if (config && config.lint && Array.isArray(config.lint)) { + config.lint.forEach(lint => { + tsConfigs.push(lint.project); + if (lint.exclude) { + if (typeof lint.exclude === 'string') { + excludes.push(lint.exclude); + } else { + lint.exclude.forEach(ex => excludes.push(ex)); + } + } + }); + } + + const removeDupes = (items: string[]) => items.reduce((newItems, item) => { + if (newItems.indexOf(item) === -1) { + newItems.push(item); + } + + return newItems; + }, []); + + // Tslint target + const lintOptions: JsonObject = { + tsConfig: removeDupes(tsConfigs).filter(t => t.indexOf('e2e') === -1), + exclude: removeDupes(excludes), + }; + architect.lint = { + builder: `${builderPackage}:tslint`, + options: lintOptions, + }; + + // server target + const serverApp = serverApps + .filter(serverApp => app.root === serverApp.root && app.index === serverApp.index)[0]; + + if (serverApp) { + const serverOptions: JsonObject = { + outputPath: serverApp.outDir || defaults.serverOutDir, + main: serverApp.main || defaults.serverMain, + tsConfig: serverApp.tsconfig || defaults.serverTsConfig, + }; + const serverTarget: JsonObject = { + builder: '@angular-devkit/build-angular:server', + options: serverOptions, + }; + architect.server = serverTarget; + } + const e2eProject: JsonObject = { + root: project.root, + projectType: 'application', + cli: {}, + schematics: {}, + }; + + const e2eArchitect: JsonObject = {}; + + // tslint:disable-next-line:max-line-length + const protractorConfig = config && config.e2e && config.e2e.protractor && config.e2e.protractor.config + ? config.e2e.protractor.config + : ''; + const e2eOptions: JsonObject = { + protractorConfig: protractorConfig, + devServerTarget: `${name}:serve`, + }; + const e2eTarget: JsonObject = { + builder: `${builderPackage}:protractor`, + options: e2eOptions, + }; + + e2eArchitect.e2e = e2eTarget; + const e2eLintOptions: JsonObject = { + tsConfig: removeDupes(tsConfigs).filter(t => t.indexOf('e2e') !== -1), + exclude: removeDupes(excludes), + }; + const e2eLintTarget: JsonObject = { + builder: `${builderPackage}:tslint`, + options: e2eLintOptions, + }; + e2eArchitect.lint = e2eLintTarget; + if (protractorConfig) { + e2eProject.architect = e2eArchitect; + } + + return { name, project, e2eProject }; + }) + .reduce((projects, mappedApp) => { + const {name, project, e2eProject} = mappedApp; + projects[name] = project; + projects[name + '-e2e'] = e2eProject; + + return projects; + }, {} as JsonObject); + + return projectMap; +} + +function updateSpecTsConfig(config: CliConfig): Rule { + return (host: Tree, context: SchematicContext) => { + const apps = config.apps || []; + apps.forEach((app: AppConfig, idx: number) => { + const tsSpecConfigPath = + join(app.root as Path, app.testTsconfig || defaults.testTsConfig); + const buffer = host.read(tsSpecConfigPath); + if (!buffer) { + return; + } + const tsCfg = JSON.parse(buffer.toString()); + if (!tsCfg.files) { + tsCfg.files = []; + } + + // Ensure the spec tsconfig contains the polyfills file + if (tsCfg.files.indexOf(app.polyfills || defaults.polyfills) === -1) { + tsCfg.files.push(app.polyfills || defaults.polyfills); + host.overwrite(tsSpecConfigPath, JSON.stringify(tsCfg, null, 2)); + } + }); + }; +} + +function updatePackageJson(packageManager?: string) { + return (host: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = host.read(pkgPath); + if (buffer == null) { + throw new SchematicsException('Could not read package.json'); + } + const content = buffer.toString(); + const pkg = JSON.parse(content); + + if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) { + throw new SchematicsException('Error reading package.json'); + } + if (!pkg.devDependencies) { + pkg.devDependencies = {}; + } + + pkg.devDependencies['@angular-devkit/build-angular'] = latestVersions.DevkitBuildAngular; + + host.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + + if (packageManager && !['npm', 'yarn', 'cnpm'].includes(packageManager)) { + packageManager = undefined; + } + context.addTask(new NodePackageInstallTask({ packageManager })); + + return host; + }; +} + +function updateTsLintConfig(): Rule { + return (host: Tree, context: SchematicContext) => { + const tsLintPath = '/tslint.json'; + const buffer = host.read(tsLintPath); + if (!buffer) { + return; + } + const tsCfg = JSON.parse(buffer.toString()); + + if (tsCfg.rules && tsCfg.rules['import-blacklist'] && + tsCfg.rules['import-blacklist'].indexOf('rxjs') !== -1) { + + tsCfg.rules['import-blacklist'] = tsCfg.rules['import-blacklist'] + .filter((rule: string | boolean) => rule !== 'rxjs'); + + host.overwrite(tsLintPath, JSON.stringify(tsCfg, null, 2)); + } + + return host; + }; +} + +export default function (): Rule { + return (host: Tree, context: SchematicContext) => { + const configPath = getConfigPath(host); + const configBuffer = host.read(normalize(configPath)); + if (configBuffer == null) { + throw new SchematicsException(`Could not find configuration file (${configPath})`); + } + const config = JSON.parse(configBuffer.toString()); + + return chain([ + migrateKarmaConfiguration(config), + migrateConfiguration(config), + updateSpecTsConfig(config), + updatePackageJson(config.packageManager), + updateTsLintConfig(), + ])(host, context); + }; +} diff --git a/packages/schematics/angular/migrations/update-6/index_spec.ts b/packages/schematics/angular/migrations/update-6/index_spec.ts new file mode 100644 index 0000000000..b19fa3b5af --- /dev/null +++ b/packages/schematics/angular/migrations/update-6/index_spec.ts @@ -0,0 +1,774 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '@angular-devkit/core'; +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; + + +describe('Migration to v6', () => { + const schematicRunner = new SchematicTestRunner( + 'migrations', + path.join(__dirname, '../migration-collection.json'), + ); + + // tslint:disable-next-line:no-any + let baseConfig: any; + const defaultOptions = {}; + let tree: UnitTestTree; + const oldConfigPath = `/.angular-cli.json`; + const configPath = `/angular.json`; + + beforeEach(() => { + baseConfig = { + schema: './node_modules/@angular/cli/lib/config/schema.json', + project: { + name: 'foo', + }, + apps: [ + { + root: 'src', + outDir: 'dist', + assets: [ + 'assets', + 'favicon.ico', + { glob: '**/*', input: './assets/', output: './assets/' }, + { glob: 'favicon.ico', input: './', output: './' }, + ], + index: 'index.html', + main: 'main.ts', + polyfills: 'polyfills.ts', + test: 'test.ts', + tsconfig: 'tsconfig.app.json', + testTsconfig: 'tsconfig.spec.json', + prefix: 'app', + styles: [ + 'styles.css', + ], + scripts: [], + environmentSource: 'environments/environment.ts', + environments: { + dev: 'environments/environment.ts', + prod: 'environments/environment.prod.ts', + }, + }, + ], + e2e: { + protractor: { + config: './protractor.conf.js', + }, + }, + lint: [ + { + project: 'src/tsconfig.app.json', + exclude: '**/node_modules/**', + }, + { + project: 'src/tsconfig.spec.json', + exclude: '**/node_modules/**', + }, + { + project: 'e2e/tsconfig.e2e.json', + exclude: '**/node_modules/**', + }, + ], + test: { + karma: { + config: './karma.conf.js', + }, + }, + defaults: { + styleExt: 'css', + build: { + namedChunks: true, + }, + serve: { + port: 8080, + }, + }, + }; + tree = new UnitTestTree(new EmptyTree()); + const packageJson = { + devDependencies: {}, + }; + tree.create('/package.json', JSON.stringify(packageJson, null, 2)); + + // Create a prod environment. + tree.create('/src/environments/environment.prod.ts', ` + export const environment = { + production: true + }; + `); + tree.create('/src/favicon.ico', ''); + }); + + // tslint:disable-next-line:no-any + function getConfig(tree: UnitTestTree): any { + return JSON.parse(tree.readContent(configPath)); + } + + describe('file creation/deletion', () => { + it('should delete the old config file', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + expect(tree.exists(oldConfigPath)).toEqual(false); + }); + + it('should create the new config file', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + expect(tree.exists(configPath)).toEqual(true); + }); + + }); + + describe('config file contents', () => { + it('should set root values', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(config.version).toEqual(1); + expect(config.newProjectRoot).toEqual('projects'); + }); + + describe('schematics', () => { + it('should define schematics collection root', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(config.schematics['@schematics/angular:component']).toBeDefined(); + }); + + function getSchematicConfig(host: UnitTestTree, name: string): JsonObject { + return getConfig(host).schematics['@schematics/angular:' + name]; + } + + describe('component config', () => { + it('should move prefix', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.prefix).toEqual('app'); + }); + + it('should move styleExt to component', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.styleext).toEqual('css'); + }); + + it('should move inlineStyle', () => { + baseConfig.defaults.component = { inlineStyle: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.inlineStyle).toEqual(true); + }); + + it('should not move inlineStyle if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.inlineStyle).toBeUndefined(); + }); + + it('should move inlineTemplate', () => { + baseConfig.defaults.component = { inlineTemplate: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.inlineTemplate).toEqual(true); + }); + + it('should not move inlineTemplate if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.inlineTemplate).toBeUndefined(); + }); + + it('should move flat', () => { + baseConfig.defaults.component = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.flat).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.component = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.spec).toBeUndefined(); + }); + + it('should move viewEncapsulation', () => { + baseConfig.defaults.component = { viewEncapsulation: 'Native' }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.viewEncapsulation).toEqual('Native'); + }); + + it('should not move viewEncapsulation if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.viewEncapsulation).toBeUndefined(); + }); + + it('should move changeDetection', () => { + baseConfig.defaults.component = { changeDetection: 'OnPush' }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.changeDetection).toEqual('OnPush'); + }); + + it('should not move changeDetection if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'component'); + expect(config.changeDetection).toBeUndefined(); + }); + }); + + describe('directive config', () => { + it('should move prefix', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'directive'); + expect(config.prefix).toEqual('app'); + }); + + it('should move flat', () => { + baseConfig.defaults.directive = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'directive'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'directive'); + expect(config.flat).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.directive = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'directive'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'directive'); + expect(config.spec).toBeUndefined(); + }); + }); + + describe('class config', () => { + it('should move spec', () => { + baseConfig.defaults.class = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'class'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'class'); + expect(config).toBeUndefined(); + }); + }); + + describe('guard config', () => { + it('should move flat', () => { + baseConfig.defaults.guard = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'guard'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'guard'); + expect(config).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.guard = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'guard'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'guard'); + expect(config).toBeUndefined(); + }); + }); + + describe('interface config', () => { + it('should move flat', () => { + baseConfig.defaults.interface = { prefix: 'I' }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'interface'); + expect(config.prefix).toEqual('I'); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'interface'); + expect(config).toBeUndefined(); + }); + }); + + describe('module config', () => { + it('should move flat', () => { + baseConfig.defaults.module = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'module'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'module'); + expect(config).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.module = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'module'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'module'); + expect(config).toBeUndefined(); + }); + }); + + describe('pipe config', () => { + it('should move flat', () => { + baseConfig.defaults.pipe = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'pipe'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'pipe'); + expect(config).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.pipe = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'pipe'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'pipe'); + expect(config).toBeUndefined(); + }); + }); + + describe('service config', () => { + it('should move flat', () => { + baseConfig.defaults.service = { flat: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'service'); + expect(config.flat).toEqual(true); + }); + + it('should not move flat if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'service'); + expect(config).toBeUndefined(); + }); + + it('should move spec', () => { + baseConfig.defaults.service = { spec: true }; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'service'); + expect(config.spec).toEqual(true); + }); + + it('should not move spec if not defined', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getSchematicConfig(tree, 'service'); + expect(config).toBeUndefined(); + }); + }); + }); + + describe('architect', () => { + it('should exist', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(config.architect).not.toBeDefined(); + }); + }); + + describe('app projects', () => { + it('should create two projects per app', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(Object.keys(config.projects).length).toEqual(2); + }); + + it('should create two projects per app', () => { + baseConfig.apps.push(baseConfig.apps[0]); + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(Object.keys(config.projects).length).toEqual(4); + }); + + it('should use the app name if defined', () => { + baseConfig.apps[0].name = 'foo'; + baseConfig.apps.push(baseConfig.apps[0]); + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(config.projects.foo).toBeDefined(); + expect(config.projects['foo-e2e']).toBeDefined(); + }); + + it('should set the project root values', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const project = getConfig(tree).projects.foo; + expect(project.root).toEqual(''); + expect(project.projectType).toEqual('application'); + }); + + it('should set build target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const build = getConfig(tree).projects.foo.architect.build; + expect(build.builder).toEqual('@angular-devkit/build-angular:browser'); + expect(build.options.scripts).toEqual([]); + expect(build.options.styles).toEqual(['src/styles.css']); + expect(build.options.assets).toEqual([ + { glob: '**/*', input: 'src/assets', output: '/assets' }, + { glob: 'favicon.ico', input: 'src', output: '/' }, + { glob: '**/*', input: 'src/assets', output: '/assets' }, + { glob: 'favicon.ico', input: 'src', output: '/' }, + ]); + expect(build.options.namedChunks).toEqual(true); + expect(build.configurations).toEqual({ + production: { + optimization: true, + outputHashing: 'all', + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + fileReplacements: [{ + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }], + }, + }); + }); + + it('should add serviceWorker to production configuration', () => { + baseConfig.apps[0].serviceWorker = true; + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + expect(config.projects.foo.architect.build.options.serviceWorker).toBeUndefined(); + expect( + config.projects.foo.architect.build.configurations.production.serviceWorker, + ).toBe(true); + }); + + it('should set the serve target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const serve = getConfig(tree).projects.foo.architect.serve; + expect(serve.builder).toEqual('@angular-devkit/build-angular:dev-server'); + expect(serve.options).toEqual({ + browserTarget: 'foo:build', + port: 8080, + }); + const prodConfig = serve.configurations.production; + expect(prodConfig.browserTarget).toEqual('foo:build:production'); + }); + + it('should set the test target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const test = getConfig(tree).projects.foo.architect['test']; + expect(test.builder).toEqual('@angular-devkit/build-angular:karma'); + expect(test.options.main).toEqual('src/test.ts'); + expect(test.options.polyfills).toEqual('src/polyfills.ts'); + expect(test.options.tsConfig).toEqual('src/tsconfig.spec.json'); + expect(test.options.karmaConfig).toEqual('./karma.conf.js'); + expect(test.options.scripts).toEqual([]); + expect(test.options.styles).toEqual(['src/styles.css']); + expect(test.options.assets).toEqual([ + { glob: '**/*', input: 'src/assets', output: '/assets' }, + { glob: 'favicon.ico', input: 'src', output: '/' }, + { glob: '**/*', input: 'src/assets', output: '/assets' }, + { glob: 'favicon.ico', input: 'src', output: '/' }, + ]); + }); + + it('should set the extract i18n target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const extract = getConfig(tree).projects.foo.architect['extract-i18n']; + expect(extract.builder).toEqual('@angular-devkit/build-angular:extract-i18n'); + expect(extract.options).toBeDefined(); + expect(extract.options.browserTarget).toEqual(`foo:build` ); + }); + + it('should set the lint target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const tslint = getConfig(tree).projects.foo.architect['lint']; + expect(tslint.builder).toEqual('@angular-devkit/build-angular:tslint'); + expect(tslint.options).toBeDefined(); + expect(tslint.options.tsConfig) + .toEqual(['src/tsconfig.app.json', 'src/tsconfig.spec.json']); + expect(tslint.options.exclude).toEqual([ '**/node_modules/**' ]); + }); + }); + + describe('e2e projects', () => { + it('should set the project root values', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const e2e = getConfig(tree).projects['foo-e2e'].architect.e2e; + expect(e2e.builder).toEqual('@angular-devkit/build-angular:protractor'); + const options = e2e.options; + expect(options.protractorConfig).toEqual('./protractor.conf.js'); + expect(options.devServerTarget).toEqual('foo:serve'); + }); + + it('should set the lint target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const tslint = getConfig(tree).projects['foo-e2e'].architect.lint; + expect(tslint.builder).toEqual('@angular-devkit/build-angular:tslint'); + expect(tslint.options).toBeDefined(); + expect(tslint.options.tsConfig).toEqual(['e2e/tsconfig.e2e.json']); + expect(tslint.options.exclude).toEqual([ '**/node_modules/**' ]); + }); + }); + }); + + describe('karma config', () => { + const karmaPath = '/karma.conf.js'; + beforeEach(() => { + tree.create(karmaPath, ` + // @angular/cli + // reports + `); + }); + + it('should replace references to "@angular/cli"', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const content = tree.readContent(karmaPath); + expect(content).not.toContain('@angular/cli'); + expect(content).toContain('@angular-devkit/build-angular'); + }); + + it('should replace references to "reports"', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const content = tree.readContent(karmaPath); + expect(content).toContain(`dir: require('path').join(__dirname, 'coverage'), reports`); + }); + }); + + describe('spec ts config', () => { + const testTsconfigPath = '/src/tsconfig.spec.json'; + beforeEach(() => { + tree.create(testTsconfigPath, ` + { + "files": [ "test.ts" ] + } + `); + }); + + it('should add polyfills', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const content = tree.readContent(testTsconfigPath); + expect(content).toContain('polyfills.ts'); + const config = JSON.parse(content); + expect(config.files.length).toEqual(2); + expect(config.files[1]).toEqual('polyfills.ts'); + }); + + it('should not add polyfills it if it already exists', () => { + tree.overwrite(testTsconfigPath, ` + { + "files": [ "test.ts", "polyfills.ts" ] + } + `); + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const content = tree.readContent(testTsconfigPath); + expect(content).toContain('polyfills.ts'); + const config = JSON.parse(content); + expect(config.files.length).toEqual(2); + }); + }); + + describe('package.json', () => { + it('should add a dev dependency to @angular-devkit/build-angular', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const content = tree.readContent('/package.json'); + const pkg = JSON.parse(content); + expect(pkg.devDependencies['@angular-devkit/build-angular']).toBeDefined(); + }); + }); + + describe('tslint.json', () => { + const tslintPath = '/tslint.json'; + // tslint:disable-next-line:no-any + let tslintConfig: any; + beforeEach(() => { + tslintConfig = { + rules: { + 'import-blacklist': ['rxjs'], + }, + }; + }); + + it('should remove "rxjs" from the "import-blacklist" rule', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree.create(tslintPath, JSON.stringify(tslintConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const tslint = JSON.parse(tree.readContent(tslintPath)); + const blacklist = tslint.rules['import-blacklist']; + expect(blacklist).toEqual([]); + }); + + it('should work if "rxjs" is not in the "import-blacklist" rule', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tslintConfig.rules['import-blacklist'] = []; + tree.create(tslintPath, JSON.stringify(tslintConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const tslint = JSON.parse(tree.readContent(tslintPath)); + const blacklist = tslint.rules['import-blacklist']; + expect(blacklist).toEqual([]); + }); + }); + + describe('server/universal apps', () => { + let serverApp; + beforeEach(() => { + serverApp = { + platform: 'server', + root: 'src', + outDir: 'dist/server', + assets: [ + 'assets', + 'favicon.ico', + ], + index: 'index.html', + main: 'main.server.ts', + test: 'test.ts', + tsconfig: 'tsconfig.server.json', + testTsconfig: 'tsconfig.spec.json', + prefix: 'app', + styles: [ + 'styles.css', + ], + scripts: [], + environmentSource: 'environments/environment.ts', + environments: { + dev: 'environments/environment.ts', + prod: 'environments/environment.prod.ts', + }, + }; + baseConfig.apps.push(serverApp); + }); + + it('should not create a separate app for server apps', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + const appCount = Object.keys(config.projects).length; + expect(appCount).toEqual(2); + }); + + it('should create a server target', () => { + tree.create(oldConfigPath, JSON.stringify(baseConfig, null, 2)); + tree = schematicRunner.runSchematic('migration-01', defaultOptions, tree); + const config = getConfig(tree); + const target = config.projects.foo.architect.server; + expect(target).toBeDefined(); + expect(target.builder).toEqual('@angular-devkit/build-angular:server'); + expect(target.options.outputPath).toEqual('dist/server'); + expect(target.options.main).toEqual('main.server.ts'); + expect(target.options.tsConfig).toEqual('tsconfig.server.json'); + }); + }); +}); diff --git a/packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts similarity index 100% rename from packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts rename to packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts diff --git a/packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.module.spec.ts b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.spec.ts similarity index 100% rename from packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.module.spec.ts rename to packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.spec.ts diff --git a/packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.module.ts b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts similarity index 100% rename from packages/schematics/angular/module/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.module.ts rename to packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts diff --git a/packages/schematics/angular/module/index.ts b/packages/schematics/angular/module/index.ts index 3c853146f7..f415419237 100644 --- a/packages/schematics/angular/module/index.ts +++ b/packages/schematics/angular/module/index.ts @@ -24,7 +24,9 @@ import { import * as ts from 'typescript'; import { addImportToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +import { getWorkspace } from '../utility/config'; import { findModuleFromOptions } from '../utility/find-module'; +import { parseName } from '../utility/parse-name'; import { Schema as ModuleOptions } from './schema'; @@ -44,7 +46,7 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule { const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); const importModulePath = normalize( - `/${options.sourceDir}/${options.path}/` + `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + '.module', @@ -69,17 +71,24 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule { } export default function (options: ModuleOptions): Rule { - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } if (options.module) { options.module = findModuleFromOptions(host, options); } + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), options.routing ? noop() : filter(path => !path.endsWith('-routing.module.ts')), @@ -88,7 +97,7 @@ export default function (options: ModuleOptions): Rule { 'if-flat': (s: string) => options.flat ? '' : s, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); return chain([ diff --git a/packages/schematics/angular/module/index_spec.ts b/packages/schematics/angular/module/index_spec.ts index 6cab95e0d9..35d02fa277 100644 --- a/packages/schematics/angular/module/index_spec.ts +++ b/packages/schematics/angular/module/index_spec.ts @@ -5,13 +5,13 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ModuleOptions } from './schema'; - +// tslint:disable:max-line-length describe('Module Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', @@ -19,18 +19,30 @@ describe('Module Schematic', () => { ); const defaultOptions: ModuleOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', spec: true, module: undefined, flat: false, }; - let appTree: Tree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a module', () => { @@ -38,15 +50,15 @@ describe('Module Schematic', () => { const tree = schematicRunner.runSchematic('module', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.module.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.module.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); }); it('should import into another module', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('module', options, appTree); - const content = getFileContent(tree, '/src/app/app.module.ts'); + const content = tree.readContent('/projects/bar/src/app/app.module.ts'); expect(content).toMatch(/import { FooModule } from '.\/foo\/foo.module'/); expect(content).toMatch(/imports: \[[^\]]*FooModule[^\]]*\]/m); }); @@ -56,19 +68,17 @@ describe('Module Schematic', () => { tree = schematicRunner.runSchematic('module', { ...defaultOptions, - path: 'app/sub1', - appRoot: 'app', + path: 'projects/bar/src/app/sub1', name: 'test1', }, tree); tree = schematicRunner.runSchematic('module', { ...defaultOptions, - path: 'app/sub2', - appRoot: 'app', + path: 'projects/bar/src/app/sub2', name: 'test2', - module: 'sub1/test1', + module: '../sub1/test1', }, tree); - const content = getFileContent(tree, '/src/app/sub1/test1/test1.module.ts'); + const content = tree.readContent('/projects/bar/src/app/sub1/test1/test1.module.ts'); expect(content).toMatch(/import { Test2Module } from '..\/..\/sub2\/test2\/test2.module'/); }); @@ -77,11 +87,11 @@ describe('Module Schematic', () => { const tree = schematicRunner.runSchematic('module', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo-routing.module.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/src/app/foo/foo.module.ts'); + expect(files.indexOf('/projects/bar/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo-routing.module.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = tree.readContent('/projects/bar/src/app/foo/foo.module.ts'); expect(moduleContent).toMatch(/import { FooRoutingModule } from '.\/foo-routing.module'/); - const routingModuleContent = getFileContent(tree, '/src/app/foo/foo-routing.module.ts'); + const routingModuleContent = tree.readContent('/projects/bar/src/app/foo/foo-routing.module.ts'); expect(routingModuleContent).toMatch(/RouterModule.forChild\(routes\)/); }); @@ -90,8 +100,8 @@ describe('Module Schematic', () => { const tree = schematicRunner.runSchematic('module', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.module.spec.ts')).toEqual(-1); + expect(files.indexOf('/projects/bar/src/app/foo/foo.module.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.module.spec.ts')).toEqual(-1); }); it('should dasherize a name', () => { @@ -99,7 +109,9 @@ describe('Module Schematic', () => { const tree = schematicRunner.runSchematic('module', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/two-word/two-word.module.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/two-word/two-word.module.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/two-word/two-word.module.ts')) + .toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/two-word/two-word.module.spec.ts')) + .toBeGreaterThanOrEqual(0); }); }); diff --git a/packages/schematics/angular/module/schema.d.ts b/packages/schematics/angular/module/schema.d.ts index 12e908551e..7d9fffae5d 100644 --- a/packages/schematics/angular/module/schema.d.ts +++ b/packages/schematics/angular/module/schema.d.ts @@ -12,17 +12,13 @@ export interface Schema { */ name: string; /** - * The path to create the module. + * The path to create the pipe. */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * Generates a routing module. */ diff --git a/packages/schematics/angular/module/schema.json b/packages/schematics/angular/module/schema.json index c124c2184f..fae4ef7b51 100644 --- a/packages/schematics/angular/module/schema.json +++ b/packages/schematics/angular/module/schema.json @@ -6,26 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the module." + "description": "The name of the module.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", - "description": "The path to create the module.", - "default": "app", + "description": "The path to create the pipe.", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "routing": { @@ -61,7 +56,5 @@ "alias": "m" } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/ng-new/index.ts b/packages/schematics/angular/ng-new/index.ts new file mode 100644 index 0000000000..1862d086f2 --- /dev/null +++ b/packages/schematics/angular/ng-new/index.ts @@ -0,0 +1,92 @@ +/** + * @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 { + Rule, + SchematicContext, + SchematicsException, + Tree, + apply, + chain, + empty, + mergeWith, + move, + schematic, +} from '@angular-devkit/schematics'; +import { + NodePackageInstallTask, + NodePackageLinkTask, + RepositoryInitializerTask, +} from '@angular-devkit/schematics/tasks'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; +import { Schema as NgNewOptions } from './schema'; + + +export default function (options: NgNewOptions): Rule { + if (!options.name) { + throw new SchematicsException(`Invalid options, "name" is required.`); + } + + if (!options.directory) { + options.directory = options.name; + } + + const workspaceOptions: WorkspaceOptions = { + name: options.name, + version: options.version, + newProjectRoot: options.newProjectRoot || 'projects', + }; + const applicationOptions: ApplicationOptions = { + projectRoot: '', + name: options.name, + inlineStyle: options.inlineStyle, + inlineTemplate: options.inlineTemplate, + prefix: options.prefix, + viewEncapsulation: options.viewEncapsulation, + routing: options.routing, + style: options.style, + skipTests: options.skipTests, + skipPackageJson: false, + }; + + return chain([ + mergeWith( + apply(empty(), [ + schematic('workspace', workspaceOptions), + schematic('application', applicationOptions), + move(options.directory || options.name), + tree => Tree.optimize(tree), + ]), + ), + (host: Tree, context: SchematicContext) => { + let packageTask; + if (!options.skipInstall) { + packageTask = context.addTask(new NodePackageInstallTask(options.directory)); + if (options.linkCli) { + packageTask = context.addTask( + new NodePackageLinkTask('@angular/cli', options.directory), + [packageTask], + ); + } + } + if (!options.skipGit) { + const commit = typeof options.commit == 'object' + ? options.commit + : (!!options.commit ? {} : false); + + context.addTask( + new RepositoryInitializerTask( + options.directory, + commit, + ), + packageTask ? [packageTask] : [], + ); + } + }, + ]); +} diff --git a/packages/schematics/angular/ng-new/index_spec.ts b/packages/schematics/angular/ng-new/index_spec.ts new file mode 100644 index 0000000000..fb25a4d41f --- /dev/null +++ b/packages/schematics/angular/ng-new/index_spec.ts @@ -0,0 +1,49 @@ +/** + * @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 { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Schema as NgNewOptions } from './schema'; + + +describe('Ng New Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: NgNewOptions = { + name: 'foo', + directory: 'bar', + version: '6.0.0', + }; + + it('should create files of a workspace', () => { + const options = { ...defaultOptions }; + + const tree = schematicRunner.runSchematic('ng-new', options); + const files = tree.files; + expect(files.indexOf('/bar/angular.json')).toBeGreaterThanOrEqual(0); + }); + + it('should create files of an application', () => { + const options = { ...defaultOptions }; + + const tree = schematicRunner.runSchematic('ng-new', options); + const files = tree.files; + expect(files.indexOf('/bar/src/tsconfig.app.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/bar/src/main.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/bar/src/app/app.module.ts')).toBeGreaterThanOrEqual(0); + }); + + it('should should set the prefix in angular.json and in app.component.ts', () => { + const options = { ...defaultOptions, prefix: 'pre' }; + + const tree = schematicRunner.runSchematic('ng-new', options); + const content = tree.readContent('/bar/angular.json'); + expect(content).toMatch(/"prefix": "pre"/); + }); +}); diff --git a/packages/schematics/angular/ng-new/schema.d.ts b/packages/schematics/angular/ng-new/schema.d.ts new file mode 100644 index 0000000000..8dc3a25db9 --- /dev/null +++ b/packages/schematics/angular/ng-new/schema.d.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The directory name to create the workspace in. + */ + directory?: string; + /** + * The name of the workspace. + */ + name: string; + /** + * Skip installing dependency packages. + */ + skipInstall?: boolean; + /** + * Link CLI to global version (internal development only). + */ + linkCli?: boolean; + /** + * Skip initializing a git repository. + */ + skipGit?: boolean; + /** + * Initial repository commit information. + */ + commit?: { name: string, email: string, message?: string } | boolean; + /** + * The path where new projects will be created. + */ + newProjectRoot?: string; + /** + * The version of the Angular CLI to use. + */ + version?: string; + /** + * Specifies if the style will be in the ts file. + */ + inlineStyle?: boolean; + /** + * Specifies if the template will be in the ts file. + */ + inlineTemplate?: boolean; + /** + * Specifies the view encapsulation strategy. + */ + viewEncapsulation?: ('Emulated' | 'Native' | 'None'); + /** + * Generates a routing module. + */ + routing?: boolean; + /** + * The prefix to apply to generated selectors. + */ + prefix?: string; + /** + * The file extension to be used for style files. + */ + style?: string; + /** + * Skip creating spec files. + */ + skipTests?: boolean; + /** + * Configure TypeScript in strict mode. + */ + strict?: boolean; +} diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json new file mode 100644 index 0000000000..0f19a1a293 --- /dev/null +++ b/packages/schematics/angular/ng-new/schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularNgNew", + "title": "Angular Ng New Options Schema", + "type": "object", + "properties": { + "directory": { + "type": "string", + "format": "path", + "description": "The directory name to create the workspace in.", + "default": "" + }, + "name": { + "description": "The name of the workspace.", + "type": "string", + "format": "html-selector", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "skipInstall": { + "description": "Skip installing dependency packages.", + "type": "boolean", + "default": false + }, + "linkCli": { + "description": "Link CLI to global version (internal development only).", + "type": "boolean", + "default": false, + "visible": false + }, + "skipGit": { + "description": "Skip initializing a git repository.", + "type": "boolean", + "default": false, + "alias": "g" + }, + "commit": { + "description": "Initial repository commit information.", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "message": { + "type": "string" + } + }, + "required": [ + "name", + "email" + ] + } + ], + "default": true + }, + "newProjectRoot": { + "description": "The path where new projects will be created.", + "type": "string", + "default": "projects" + }, + "inlineStyle": { + "description": "Specifies if the style will be in the ts file.", + "type": "boolean", + "default": false, + "alias": "s" + }, + "inlineTemplate": { + "description": "Specifies if the template will be in the ts file.", + "type": "boolean", + "default": false, + "alias": "t" + }, + "viewEncapsulation": { + "description": "Specifies the view encapsulation strategy.", + "enum": ["Emulated", "Native", "None"], + "type": "string" + }, + "version": { + "type": "string", + "description": "The version of the Angular CLI to use.", + "visible": false + }, + "routing": { + "type": "boolean", + "description": "Generates a routing module.", + "default": false + }, + "prefix": { + "type": "string", + "format": "html-selector", + "description": "The prefix to apply to generated selectors.", + "default": "app", + "alias": "p" + }, + "style": { + "description": "The file extension to be used for style files.", + "type": "string", + "default": "css" + }, + "skipTests": { + "description": "Skip creating spec files.", + "type": "boolean", + "default": false, + "alias": "S" + }, + "strict": { + "description": "Configure TypeScript in strict mode.", + "type": "boolean", + "default": false + } + }, + "required": [ + "version" + ] +} diff --git a/packages/schematics/angular/package.json b/packages/schematics/angular/package.json index 9cf10dc6fd..81b8d7aab9 100644 --- a/packages/schematics/angular/package.json +++ b/packages/schematics/angular/package.json @@ -12,10 +12,8 @@ }, "schematics": "./collection.json", "dependencies": { - "typescript": "~2.6.2" - }, - "peerDependencies": { "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "0.0.0" + "@angular-devkit/schematics": "0.0.0", + "typescript": ">=2.6.2 <2.8" } } diff --git a/packages/schematics/angular/pipe/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.pipe.spec.ts b/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.spec.ts similarity index 100% rename from packages/schematics/angular/pipe/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.pipe.spec.ts rename to packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.spec.ts diff --git a/packages/schematics/angular/pipe/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts b/packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts similarity index 100% rename from packages/schematics/angular/pipe/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts rename to packages/schematics/angular/pipe/files/__name@dasherize@if-flat__/__name@dasherize__.pipe.ts diff --git a/packages/schematics/angular/pipe/index.ts b/packages/schematics/angular/pipe/index.ts index 5887923330..9de83edf8d 100644 --- a/packages/schematics/angular/pipe/index.ts +++ b/packages/schematics/angular/pipe/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -24,7 +24,9 @@ import { import * as ts from 'typescript'; import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; +import { getWorkspace } from '../utility/config'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { parseName } from '../utility/parse-name'; import { Schema as PipeOptions } from './schema'; @@ -42,7 +44,7 @@ function addDeclarationToNgModule(options: PipeOptions): Rule { const sourceText = text.toString('utf-8'); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); - const pipePath = `/${options.sourceDir}/${options.path}/` + const pipePath = `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + '.pipe'; @@ -84,13 +86,21 @@ function addDeclarationToNgModule(options: PipeOptions): Rule { } export default function (options: PipeOptions): Rule { - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; + options.module = findModuleFromOptions(host, options); const templateSource = apply(url('./files'), [ @@ -100,14 +110,12 @@ export default function (options: PipeOptions): Rule { 'if-flat': (s: string) => options.flat ? '' : s, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); - return chain([ - branchAndMerge(chain([ + return branchAndMerge(chain([ addDeclarationToNgModule(options), mergeWith(templateSource), - ])), - ])(host, context); + ]))(host, context); }; } diff --git a/packages/schematics/angular/pipe/index_spec.ts b/packages/schematics/angular/pipe/index_spec.ts index db75607fc6..4991ce809e 100644 --- a/packages/schematics/angular/pipe/index_spec.ts +++ b/packages/schematics/angular/pipe/index_spec.ts @@ -5,10 +5,11 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; +import { Schema as ApplicationOptions } from '../application/schema'; import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as PipeOptions } from './schema'; @@ -19,19 +20,31 @@ describe('Pipe Schematic', () => { ); const defaultOptions: PipeOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', spec: true, module: undefined, export: false, flat: true, }; - let appTree: Tree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a pipe', () => { @@ -39,9 +52,9 @@ describe('Pipe Schematic', () => { const tree = schematicRunner.runSchematic('pipe', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo.pipe.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo.pipe.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); + expect(files.indexOf('/projects/bar/src/app/foo.pipe.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo.pipe.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = getFileContent(tree, '/projects/bar/src/app/app.module.ts'); expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo.pipe'/); expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooPipe\r?\n/m); }); @@ -50,13 +63,13 @@ describe('Pipe Schematic', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; const tree = schematicRunner.runSchematic('pipe', options, appTree); - const appModule = getFileContent(tree, '/src/app/app.module.ts'); + const appModule = getFileContent(tree, '/projects/bar/src/app/app.module.ts'); expect(appModule).toMatch(/import { FooPipe } from '.\/foo.pipe'/); }); it('should fail if specified module does not exist', () => { - const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; + const options = { ...defaultOptions, module: '/projects/bar/src/app/app.moduleXXX.ts' }; let thrownError: Error | null = null; try { schematicRunner.runSchematic('pipe', options, appTree); @@ -70,7 +83,7 @@ describe('Pipe Schematic', () => { const options = { ...defaultOptions, export: true }; const tree = schematicRunner.runSchematic('pipe', options, appTree); - const appModuleContent = getFileContent(tree, '/src/app/app.module.ts'); + const appModuleContent = getFileContent(tree, '/projects/bar/src/app/app.module.ts'); expect(appModuleContent).toMatch(/exports: \[FooPipe\]/); }); @@ -79,16 +92,16 @@ describe('Pipe Schematic', () => { const tree = schematicRunner.runSchematic('pipe', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.pipe.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.pipe.ts')).toBeGreaterThanOrEqual(0); - const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); + expect(files.indexOf('/projects/bar/src/app/foo/foo.pipe.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.pipe.ts')).toBeGreaterThanOrEqual(0); + const moduleContent = getFileContent(tree, '/projects/bar/src/app/app.module.ts'); expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.pipe'/); expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooPipe\r?\n/m); }); it('should use the module flag even if the module is a routing module', () => { const routingFileName = 'app-routing.module.ts'; - const routingModulePath = `/src/app/${routingFileName}`; + const routingModulePath = `/projects/bar/src/app/${routingFileName}`; const newTree = createAppModule(appTree, routingModulePath); const options = { ...defaultOptions, module: routingFileName }; const tree = schematicRunner.runSchematic('pipe', options, newTree); diff --git a/packages/schematics/angular/pipe/schema.d.ts b/packages/schematics/angular/pipe/schema.d.ts index 562afd6ecb..70a37a9f3c 100644 --- a/packages/schematics/angular/pipe/schema.d.ts +++ b/packages/schematics/angular/pipe/schema.d.ts @@ -16,13 +16,9 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * Flag to indicate if a dir is created. */ diff --git a/packages/schematics/angular/pipe/schema.json b/packages/schematics/angular/pipe/schema.json index 009fe827cf..e57787103d 100644 --- a/packages/schematics/angular/pipe/schema.json +++ b/packages/schematics/angular/pipe/schema.json @@ -6,26 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the pipe." + "description": "The name of the pipe.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", "description": "The path to create the pipe.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "flat": { @@ -55,7 +50,5 @@ "description": "Specifies if declaring module exports the pipe." } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/application/files/__sourcedir__/ngsw-config.json b/packages/schematics/angular/service-worker/files/ngsw-config.json similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/ngsw-config.json rename to packages/schematics/angular/service-worker/files/ngsw-config.json diff --git a/packages/schematics/angular/service-worker/index.ts b/packages/schematics/angular/service-worker/index.ts new file mode 100644 index 0000000000..e8ac4d00ae --- /dev/null +++ b/packages/schematics/angular/service-worker/index.ts @@ -0,0 +1,249 @@ +/** + * @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 { + Rule, + SchematicContext, + SchematicsException, + Tree, + UpdateRecorder, + apply, + chain, + mergeWith, + move, + template, + url, +} from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { addSymbolToNgModuleMetadata, isImported } from '../utility/ast-utils'; +import { InsertChange } from '../utility/change'; +import { + getWorkspace, + getWorkspacePath, +} from '../utility/config'; +import { getAppModulePath } from '../utility/ng-ast-utils'; +import { insertImport } from '../utility/route-utils'; +import { Schema as ServiceWorkerOptions } from './schema'; + +const packageJsonPath = '/package.json'; + +function updateConfigFile(options: ServiceWorkerOptions): Rule { + return (host: Tree, context: SchematicContext) => { + context.logger.debug('updating config file.'); + const workspacePath = getWorkspacePath(host); + + const workspace = getWorkspace(host); + + const project = workspace.projects[options.project]; + + if (!project) { + throw new Error(`Project is not defined in this workspace.`); + } + + if (!project.architect) { + throw new Error(`Architect is not defined for this project.`); + } + + if (!project.architect[options.target]) { + throw new Error(`Target is not defined for this project.`); + } + + let applyTo = project.architect[options.target].options; + + if (options.configuration && + project.architect[options.target].configurations && + project.architect[options.target].configurations[options.configuration]) { + applyTo = project.architect[options.target].configurations[options.configuration]; + } + + applyTo.serviceWorker = true; + + host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); + + return host; + }; +} + +function addDependencies(): Rule { + return (host: Tree, context: SchematicContext) => { + const packageName = '@angular/service-worker'; + context.logger.debug(`adding dependency (${packageName})`); + const buffer = host.read(packageJsonPath); + if (buffer === null) { + throw new SchematicsException('Could not find package.json'); + } + + const packageObject = JSON.parse(buffer.toString()); + + const ngCoreVersion = packageObject.dependencies['@angular/core']; + packageObject.dependencies[packageName] = ngCoreVersion; + + host.overwrite(packageJsonPath, JSON.stringify(packageObject, null, 2)); + + return host; + }; +} + +function updateAppModule(options: ServiceWorkerOptions): Rule { + return (host: Tree, context: SchematicContext) => { + context.logger.debug('Updating appmodule'); + + // find app module + const workspace = getWorkspace(host); + const project = workspace.projects[options.project]; + if (!project.architect) { + throw new Error('Project architect not found.'); + } + const mainPath = project.architect.build.options.main; + const modulePath = getAppModulePath(host, mainPath); + context.logger.debug(`module path: ${modulePath}`); + + // add import + let moduleSource = getTsSourceFile(host, modulePath); + let importModule = 'ServiceWorkerModule'; + let importPath = '@angular/service-worker'; + if (!isImported(moduleSource, importModule, importPath)) { + const change = insertImport + (moduleSource, modulePath, importModule, importPath); + if (change) { + const recorder = host.beginUpdate(modulePath); + recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd); + host.commitUpdate(recorder); + } + } + + // add import for environments + // import { environment } from '../environments/environment'; + moduleSource = getTsSourceFile(host, modulePath); + importModule = 'environment'; + // TODO: dynamically find environments relative path + importPath = '../environments/environment'; + if (!isImported(moduleSource, importModule, importPath)) { + const change = insertImport + (moduleSource, modulePath, importModule, importPath); + if (change) { + const recorder = host.beginUpdate(modulePath); + recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd); + host.commitUpdate(recorder); + } + } + + // register SW in app module + const importText = + `ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production })`; + moduleSource = getTsSourceFile(host, modulePath); + const metadataChanges = addSymbolToNgModuleMetadata( + moduleSource, modulePath, 'imports', importText); + if (metadataChanges) { + const recorder = host.beginUpdate(modulePath); + metadataChanges.forEach((change: InsertChange) => { + recorder.insertRight(change.pos, change.toAdd); + }); + host.commitUpdate(recorder); + } + + return host; + }; +} + +function getTsSourceFile(host: Tree, path: string): ts.SourceFile { + const buffer = host.read(path); + if (!buffer) { + throw new SchematicsException(`Could not read file (${path}).`); + } + const content = buffer.toString(); + const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); + + return source; +} + +function updateIndexFile(options: ServiceWorkerOptions): Rule { + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + const project = workspace.projects[options.project]; + let path: string; + if (project && project.architect && project.architect.build && + project.architect.build.options.index) { + path = project.architect.build.options.index; + } else { + throw new SchematicsException('Could not find index file for the project'); + } + const buffer = host.read(path); + if (buffer === null) { + throw new SchematicsException(`Could not read index file: ${path}`); + } + const content = buffer.toString(); + const lines = content.split('\n'); + let closingHeadTagLineIndex = -1; + let closingHeadTagLine = ''; + lines.forEach((line, index) => { + if (/<\/head>/.test(line) && closingHeadTagLineIndex === -1) { + closingHeadTagLine = line; + closingHeadTagLineIndex = index; + } + }); + + const indent = getIndent(closingHeadTagLine) + ' '; + const itemsToAdd = [ + '', + '', + '', + ]; + + const textToInsert = itemsToAdd + .map(text => indent + text) + .join('\n'); + + const updatedIndex = [ + ...lines.slice(0, closingHeadTagLineIndex), + textToInsert, + ...lines.slice(closingHeadTagLineIndex), + ].join('\n'); + + host.overwrite(path, updatedIndex); + + return host; + }; +} + +function getIndent(text: string): string { + let indent = ''; + let hitNonSpace = false; + text.split('') + .forEach(char => { + if (char === ' ' && !hitNonSpace) { + indent += ' '; + } else { + hitNonSpace = true; + } + }, 0); + + return indent; +} + +export default function (options: ServiceWorkerOptions): Rule { + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + const project = workspace.projects[options.project]; + if (!project) { + throw new SchematicsException(`Invalid project name (${options.project})`); + } + + const templateSource = apply(url('./files'), [ + template({...options}), + move(project.root), + ]); + + return chain([ + mergeWith(templateSource), + updateConfigFile(options), + addDependencies(), + updateAppModule(options), + updateIndexFile(options), + ])(host, context); + }; +} diff --git a/packages/schematics/angular/service-worker/index_spec.ts b/packages/schematics/angular/service-worker/index_spec.ts new file mode 100644 index 0000000000..b2d0255746 --- /dev/null +++ b/packages/schematics/angular/service-worker/index_spec.ts @@ -0,0 +1,108 @@ +/** + * @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 { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; +import { Schema as ServiceWorkerOptions } from './schema'; + + +describe('Service Worker Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: ServiceWorkerOptions = { + project: 'bar', + target: 'build', + configuration: 'production', + }; + + let appTree: UnitTestTree; + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + + beforeEach(() => { + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); + }); + + it('should update the proudction configuration', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const configText = tree.readContent('/angular.json'); + const config = JSON.parse(configText); + const swFlag = config.projects.bar.architect.build.configurations.production.serviceWorker; + expect(swFlag).toEqual(true); + }); + + it('should update the target options if no configuration is set', () => { + const options = {...defaultOptions, configuration: ''}; + const tree = schematicRunner.runSchematic('service-worker', options, appTree); + const configText = tree.readContent('/angular.json'); + const config = JSON.parse(configText); + const swFlag = config.projects.bar.architect.build.options.serviceWorker; + expect(swFlag).toEqual(true); + }); + + it('should add the necessary dependency', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const pkgText = tree.readContent('/package.json'); + const pkg = JSON.parse(pkgText); + const version = pkg.dependencies['@angular/core']; + expect(pkg.dependencies['@angular/service-worker']).toEqual(version); + }); + + it('should import ServiceWorkerModule', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts'); + expect(pkgText).toMatch(/import \{ ServiceWorkerModule \} from '@angular\/service-worker'/); + }); + + it('should import environment', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts'); + expect(pkgText).toMatch(/import \{ environment \} from '\.\.\/environments\/environment'/); + }); + + it('should add the SW import to the NgModule imports', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const pkgText = tree.readContent('/projects/bar/src/app/app.module.ts'); + // tslint:disable-next-line:max-line-length + const regex = /ServiceWorkerModule\.register\('\/ngsw-worker.js\', { enabled: environment.production }\)/; + expect(pkgText).toMatch(regex); + }); + + it('should put the ngsw-config.json file in the project root', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const path = '/projects/bar/ngsw-config.json'; + expect(tree.exists(path)).toEqual(true); + }); + + it('should update the index file', () => { + const tree = schematicRunner.runSchematic('service-worker', defaultOptions, appTree); + const content = tree.readContent('projects/bar/src/index.html'); + + expect(content).toMatch(//); + expect(content).toMatch(//); + expect(content).toMatch(//); + }); +}); diff --git a/packages/schematics/angular/service-worker/schema.d.ts b/packages/schematics/angular/service-worker/schema.d.ts new file mode 100644 index 0000000000..624ffaadde --- /dev/null +++ b/packages/schematics/angular/service-worker/schema.d.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The name of the project. + */ + project: string; + /** + * ": "The target to apply service worker to. + */ + target: string; + /** + * The configuration to apply service worker to. + */ + configuration: string; +} diff --git a/packages/schematics/angular/service-worker/schema.json b/packages/schematics/angular/service-worker/schema.json new file mode 100644 index 0000000000..8f84154b17 --- /dev/null +++ b/packages/schematics/angular/service-worker/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularServiceWorker", + "title": "Angular Service Worker Options Schema", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project." + }, + "target": { + "type": "string", + "description": "The target to apply service worker to.", + "default": "build" + }, + "configuration": { + "type": "string", + "description": "The configuration to apply service worker to.", + "default": "production" + } + }, + "required": [ + "project" + ] +} \ No newline at end of file diff --git a/packages/schematics/angular/service/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.service.spec.ts b/packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.spec.ts similarity index 100% rename from packages/schematics/angular/service/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.service.spec.ts rename to packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.spec.ts diff --git a/packages/schematics/angular/service/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.service.ts b/packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.ts similarity index 74% rename from packages/schematics/angular/service/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.service.ts rename to packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.ts index f685263951..f14985e32b 100644 --- a/packages/schematics/angular/service/files/__path__/__name@dasherize@if-flat__/__name@dasherize__.service.ts +++ b/packages/schematics/angular/service/files/__name@dasherize@if-flat__/__name@dasherize__.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class <%= classify(name) %>Service { constructor() { } - } diff --git a/packages/schematics/angular/service/index.ts b/packages/schematics/angular/service/index.ts index 7c6299a421..5c2801725f 100644 --- a/packages/schematics/angular/service/index.ts +++ b/packages/schematics/angular/service/index.ts @@ -5,15 +5,12 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { strings } from '@angular-devkit/core'; import { Rule, SchematicContext, - SchematicsException, Tree, apply, - branchAndMerge, - chain, filter, mergeWith, move, @@ -21,63 +18,25 @@ import { template, url, } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { addProviderToModule } from '../utility/ast-utils'; -import { InsertChange } from '../utility/change'; -import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; +import { getWorkspace } from '../utility/config'; +import { parseName } from '../utility/parse-name'; import { Schema as ServiceOptions } from './schema'; - -function addProviderToNgModule(options: ServiceOptions): Rule { - return (host: Tree) => { - if (!options.module) { - return host; - } - - const modulePath = options.module; - if (!host.exists(options.module)) { - throw new Error('Specified module does not exist'); - } - - const text = host.read(modulePath); - if (text === null) { - throw new SchematicsException(`File ${modulePath} does not exist.`); +export default function (options: ServiceOptions): Rule { + return (host: Tree, context: SchematicContext) => { + const workspace = getWorkspace(host); + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; } - const sourceText = text.toString('utf-8'); - - const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); + const project = workspace.projects[options.project]; - const servicePath = `/${options.sourceDir}/${options.path}/` - + (options.flat ? '' : strings.dasherize(options.name) + '/') - + strings.dasherize(options.name) - + '.service'; - const relativePath = buildRelativePath(modulePath, servicePath); - const changes = addProviderToModule(source, modulePath, - strings.classify(`${options.name}Service`), - relativePath); - const recorder = host.beginUpdate(modulePath); - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; } - host.commitUpdate(recorder); - return host; - }; -} - -export default function (options: ServiceOptions): Rule { - options.path = options.path ? normalize(options.path) : options.path; - const sourceDir = options.sourceDir; - if (!sourceDir) { - throw new SchematicsException(`sourceDir option is required.`); - } - - return (host: Tree, context: SchematicContext) => { - if (options.module) { - options.module = findModuleFromOptions(host, options); - } + const parsedPath = parseName(options.path, options.name); + options.name = parsedPath.name; + options.path = parsedPath.path; const templateSource = apply(url('./files'), [ options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), @@ -86,15 +45,9 @@ export default function (options: ServiceOptions): Rule { 'if-flat': (s: string) => options.flat ? '' : s, ...options, }), - move(sourceDir), + move(parsedPath.path), ]); - return chain([ - branchAndMerge(chain([ - filter(path => path.endsWith('.module.ts') && !path.endsWith('-routing.module.ts')), - addProviderToNgModule(options), - mergeWith(templateSource), - ])), - ])(host, context); + return mergeWith(templateSource)(host, context); }; } diff --git a/packages/schematics/angular/service/index_spec.ts b/packages/schematics/angular/service/index_spec.ts index 6afe612e47..0ac6d3df44 100644 --- a/packages/schematics/angular/service/index_spec.ts +++ b/packages/schematics/angular/service/index_spec.ts @@ -5,32 +5,43 @@ * Use 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 { Tree, VirtualTree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; -import { createAppModule, getFileContent } from '../utility/test'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ServiceOptions } from './schema'; - -describe('Pipe Schematic', () => { +// tslint:disable:max-line-length +describe('Service Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', path.join(__dirname, '../collection.json'), ); const defaultOptions: ServiceOptions = { name: 'foo', - path: 'app', - sourceDir: 'src', spec: true, - module: undefined, flat: false, }; - let appTree: Tree; + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, + }; + let appTree: UnitTestTree; beforeEach(() => { - appTree = new VirtualTree(); - appTree = createAppModule(appTree); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a service', () => { @@ -38,35 +49,16 @@ describe('Pipe Schematic', () => { const tree = schematicRunner.runSchematic('service', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.service.spec.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.service.ts')).toBeGreaterThanOrEqual(0); - }); - - it('should not be provided by default', () => { - const options = { ...defaultOptions }; - - const tree = schematicRunner.runSchematic('service', options, appTree); - const content = getFileContent(tree, '/src/app/app.module.ts'); - expect(content).not.toMatch(/import { FooService } from '.\/foo\/foo.service'/); + expect(files.indexOf('/projects/bar/src/app/foo/foo.service.spec.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.service.ts')).toBeGreaterThanOrEqual(0); }); - it('should import into a specified module', () => { - const options = { ...defaultOptions, module: 'app.module.ts' }; + it('service should be tree-shakeable', () => { + const options = { ...defaultOptions}; const tree = schematicRunner.runSchematic('service', options, appTree); - const content = getFileContent(tree, '/src/app/app.module.ts'); - expect(content).toMatch(/import { FooService } from '.\/foo\/foo.service'/); - }); - - it('should fail if specified module does not exist', () => { - const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' }; - let thrownError: Error | null = null; - try { - schematicRunner.runSchematic('service', options, appTree); - } catch (err) { - thrownError = err; - } - expect(thrownError).toBeDefined(); + const content = tree.readContent('/projects/bar/src/app/foo/foo.service.ts'); + expect(content).toMatch(/providedIn: 'root'/); }); it('should respect the spec flag', () => { @@ -74,7 +66,7 @@ describe('Pipe Schematic', () => { const tree = schematicRunner.runSchematic('service', options, appTree); const files = tree.files; - expect(files.indexOf('/src/app/foo/foo.service.ts')).toBeGreaterThanOrEqual(0); - expect(files.indexOf('/src/app/foo/foo.service.spec.ts')).toEqual(-1); + expect(files.indexOf('/projects/bar/src/app/foo/foo.service.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/projects/bar/src/app/foo/foo.service.spec.ts')).toEqual(-1); }); }); diff --git a/packages/schematics/angular/service/schema.d.ts b/packages/schematics/angular/service/schema.d.ts index 068b07b7f2..0dc5a42156 100644 --- a/packages/schematics/angular/service/schema.d.ts +++ b/packages/schematics/angular/service/schema.d.ts @@ -16,13 +16,9 @@ export interface Schema { */ path?: string; /** - * The path of the source directory. + * The name of the project. */ - sourceDir?: string; - /** - * The root of the application. - */ - appRoot?: string; + project?: string; /** * Flag to indicate if a dir is created. */ @@ -31,8 +27,4 @@ export interface Schema { * Specifies if a spec file is generated. */ spec?: boolean; - /** - * Allows specification of the providing module. - */ - module?: string; } diff --git a/packages/schematics/angular/service/schema.json b/packages/schematics/angular/service/schema.json index 400f3939d8..44c0cb365e 100644 --- a/packages/schematics/angular/service/schema.json +++ b/packages/schematics/angular/service/schema.json @@ -6,26 +6,21 @@ "properties": { "name": { "type": "string", - "description": "The name of the service." + "description": "The name of the service.", + "$default": { + "$source": "argv", + "index": 0 + } }, "path": { "type": "string", "format": "path", "description": "The path to create the service.", - "default": "app", "visible": false }, - "sourceDir": { + "project": { "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "visible": false - }, - "appRoot": { - "type": "string", - "format": "path", - "description": "The root of the application.", + "description": "The name of the project.", "visible": false }, "flat": { @@ -37,15 +32,7 @@ "type": "boolean", "default": true, "description": "Specifies if a spec file is generated." - }, - "module": { - "type": "string", - "default": "", - "description": "Allows specification of the providing module.", - "alias": "m" } }, - "required": [ - "name" - ] + "required": [] } diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json b/packages/schematics/angular/universal/files/__tsconfigFileName__.json similarity index 89% rename from packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json rename to packages/schematics/angular/universal/files/__tsconfigFileName__.json index afd34261c0..552fe89351 100644 --- a/packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json +++ b/packages/schematics/angular/universal/files/__tsconfigFileName__.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "outDir": "../<%= outDir %>", + "outDir": "<%= outDir %>-server", "baseUrl": "./", "module": "commonjs", "types": [] diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts b/packages/schematics/angular/universal/files/src/__main@stripTsExtension__.ts similarity index 60% rename from packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts rename to packages/schematics/angular/universal/files/src/__main@stripTsExtension__.ts index d6f593e235..5f82b3e53a 100644 --- a/packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts +++ b/packages/schematics/angular/universal/files/src/__main@stripTsExtension__.ts @@ -6,4 +6,4 @@ if (environment.production) { enableProdMode(); } -export { <%= rootModuleClassName %> } from './<%= appDir %>/<%= stripTsExtension(rootModuleFileName) %>'; +export { <%= rootModuleClassName %> } from './app/<%= stripTsExtension(rootModuleFileName) %>'; diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__appDir__/__rootModuleFileName__ b/packages/schematics/angular/universal/files/src/app/__rootModuleFileName__ similarity index 100% rename from packages/schematics/angular/universal/files/__sourceDir__/__appDir__/__rootModuleFileName__ rename to packages/schematics/angular/universal/files/src/app/__rootModuleFileName__ diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts index 17ee3094f1..1f2bd165c9 100644 --- a/packages/schematics/angular/universal/index.ts +++ b/packages/schematics/angular/universal/index.ts @@ -5,7 +5,7 @@ * Use 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 { normalize, strings } from '@angular-devkit/core'; +import { JsonObject, experimental, normalize, parseJson, strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -14,52 +14,72 @@ import { apply, chain, mergeWith, + move, template, url, } from '@angular-devkit/schematics'; import * as ts from 'typescript'; import { findNode, getDecoratorMetadata } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; -import { AppConfig, getAppFromConfig, getConfig } from '../utility/config'; +import { getWorkspace } from '../utility/config'; import { findBootstrapModuleCall, findBootstrapModulePath } from '../utility/ng-ast-utils'; import { Schema as UniversalOptions } from './schema'; +function getWorkspacePath(host: Tree): string { + const possibleFiles = [ '/angular.json', '/.angular.json' ]; + + return possibleFiles.filter(path => host.exists(path))[0]; +} + +function getClientProject(host: Tree, options: UniversalOptions): experimental.workspace.Project { + const workspace = getWorkspace(host); + const clientProject = workspace.projects[options.clientProject]; + if (!clientProject) { + throw new SchematicsException(`Client app ${options.clientProject} not found.`); + } + + return clientProject; +} + +function getClientArchitect( + host: Tree, + options: UniversalOptions, +): experimental.workspace.Architect { + const clientArchitect = getClientProject(host, options).architect; + + if (!clientArchitect) { + throw new Error('Client project architect not found.'); + } + + return clientArchitect; +} + function updateConfigFile(options: UniversalOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const clientApp = getAppFromConfig(config, options.clientApp || '0'); - if (clientApp === null) { - throw new SchematicsException('Client app not found.'); - } - options.test = options.test || clientApp.test; - - const tsCfg = options.tsconfigFileName && options.tsconfigFileName.endsWith('.json') - ? options.tsconfigFileName : `${options.tsconfigFileName}.json`; - const testTsCfg = options.testTsconfigFileName && options.testTsconfigFileName.endsWith('.json') - ? options.testTsconfigFileName : `${options.testTsconfigFileName}.json`; - - const serverApp: AppConfig = { - ...clientApp, - platform: 'server', - root: options.root, - outDir: options.outDir, - index: options.index, - main: options.main, - test: options.test, - tsconfig: tsCfg, - testTsconfig: testTsCfg, - polyfills: undefined, + const builderOptions: JsonObject = { + outputPath: `dist/${options.clientProject}-server`, + main: `projects/${options.clientProject}/src/main.server.ts`, + tsConfig: `projects/${options.clientProject}/tsconfig.server.json`, }; - if (options.name) { - serverApp.name = options.name; + const serverTarget: JsonObject = { + builder: '@angular-devkit/build-angular:server', + options: builderOptions, + }; + const workspace = getWorkspace(host); + + if (!workspace.projects[options.clientProject]) { + throw new SchematicsException(`Client app ${options.clientProject} not found.`); } - if (!config.apps) { - config.apps = []; + const clientProject = workspace.projects[options.clientProject]; + if (!clientProject.architect) { + throw new Error('Client project architect not found.'); } - config.apps.push(serverApp); + clientProject.architect.server = serverTarget; + + const workspacePath = getWorkspacePath(host); - host.overwrite('/.angular-cli.json', JSON.stringify(config, null, 2)); + host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); return host; }; @@ -86,12 +106,8 @@ function findBrowserModuleImport(host: Tree, modulePath: string): ts.Node { function wrapBootstrapCall(options: UniversalOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const clientApp = getAppFromConfig(config, options.clientApp || '0'); - if (clientApp === null) { - throw new SchematicsException('Client app not found.'); - } - const mainPath = normalize(`/${clientApp.root}/${clientApp.main}`); + const clientArchitect = getClientArchitect(host, options); + const mainPath = normalize('/' + clientArchitect.build.options.main); let bootstrapCall: ts.Node | null = findBootstrapModuleCall(host, mainPath); if (bootstrapCall === null) { throw new SchematicsException('Bootstrap module not found.'); @@ -118,15 +134,13 @@ function wrapBootstrapCall(options: UniversalOptions): Rule { function addServerTransition(options: UniversalOptions): Rule { return (host: Tree) => { - const config = getConfig(host); - const clientApp = getAppFromConfig(config, options.clientApp || '0'); - if (clientApp === null) { - throw new SchematicsException('Client app not found.'); - } - const mainPath = normalize(`/${clientApp.root}/${clientApp.main}`); + const clientProject = getClientProject(host, options); + const clientArchitect = getClientArchitect(host, options); + const mainPath = normalize('/' + clientArchitect.build.options.main); const bootstrapModuleRelativePath = findBootstrapModulePath(host, mainPath); - const bootstrapModulePath = normalize(`/${clientApp.root}/${bootstrapModuleRelativePath}.ts`); + const bootstrapModulePath = normalize( + `/${clientProject.root}/src/${bootstrapModuleRelativePath}.ts`); const browserModuleImport = findBrowserModuleImport(host, bootstrapModulePath); const appId = options.appId; @@ -160,30 +174,37 @@ function addDependencies(): Rule { }; } -function updateGitignore(options: UniversalOptions): Rule { - return (host: Tree) => { - const ignorePath = normalize('/.gitignore'); - const buffer = host.read(ignorePath); - if (buffer === null) { - // Assumption is made that there is no git repository. - return host; - } else { - const content = buffer.toString(); - host.overwrite(ignorePath, `${content}\n${options.outDir}`); - } +function getTsConfigOutDir(host: Tree, architect: experimental.workspace.Architect): string { + const tsConfigPath = architect.build.options.tsConfig; + const tsConfigBuffer = host.read(tsConfigPath); + if (!tsConfigBuffer) { + throw new SchematicsException(`Could not read ${tsConfigPath}`); + } + const tsConfigContent = tsConfigBuffer.toString(); + const tsConfig = parseJson(tsConfigContent); + if (tsConfig === null || typeof tsConfig !== 'object' || Array.isArray(tsConfig) || + tsConfig.compilerOptions === null || typeof tsConfig.compilerOptions !== 'object' || + Array.isArray(tsConfig.compilerOptions)) { + throw new SchematicsException(`Invalid tsconfig - ${tsConfigPath}`); + } + const outDir = tsConfig.compilerOptions.outDir; - return host; - }; + return outDir as string; } export default function (options: UniversalOptions): Rule { return (host: Tree, context: SchematicContext) => { + const clientProject = getClientProject(host, options); + const clientArchitect = getClientArchitect(host, options); + const outDir = getTsConfigOutDir(host, clientArchitect); const templateSource = apply(url('./files'), [ template({ ...strings, ...options as object, stripTsExtension: (s: string) => { return s.replace(/\.ts$/, ''); }, + outDir, }), + move(clientProject.root), ]); return chain([ @@ -192,7 +213,6 @@ export default function (options: UniversalOptions): Rule { updateConfigFile(options), wrapBootstrapCall(options), addServerTransition(options), - updateGitignore(options), ])(host, context); }; } diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts index d82f9fc03a..c783384837 100644 --- a/packages/schematics/angular/universal/index_spec.ts +++ b/packages/schematics/angular/universal/index_spec.ts @@ -5,64 +5,64 @@ * Use 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 { Tree } from '@angular-devkit/schematics'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import * as path from 'path'; import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as UniversalOptions } from './schema'; - describe('Universal Schematic', () => { const schematicRunner = new SchematicTestRunner( '@schematics/angular', path.join(__dirname, '../collection.json'), ); const defaultOptions: UniversalOptions = { - name: 'foo', + clientProject: 'bar', + }; + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false, }; - let appTree: Tree; + let appTree: UnitTestTree; beforeEach(() => { - const appOptions: ApplicationOptions = { - directory: '', - name: 'universal-app', - sourceDir: 'src', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'None', - version: '1.2.3', - routing: false, - style: 'css', - skipTests: false, - minimal: false, - }; - appTree = schematicRunner.runSchematic('application', appOptions); + appTree = schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); it('should create a root module file', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/src/app/app.server.module.ts'; - const file = tree.files.filter(f => f === filePath)[0]; - expect(file).toBeDefined(); + const filePath = '/projects/bar/src/app/app.server.module.ts'; + expect(tree.exists(filePath)).toEqual(true); }); it('should create a main file', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/src/main.server.ts'; - const file = tree.files.filter(f => f === filePath)[0]; - expect(file).toBeDefined(); + const filePath = '/projects/bar/src/main.server.ts'; + expect(tree.exists(filePath)).toEqual(true); const contents = tree.readContent(filePath); expect(contents).toMatch(/export { AppServerModule } from '\.\/app\/app\.server\.module'/); }); it('should create a tsconfig file', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/src/tsconfig.server.json'; - const file = tree.files.filter(f => f === filePath)[0]; - expect(file).toBeDefined(); + const filePath = '/projects/bar/tsconfig.server.json'; + expect(tree.exists(filePath)).toEqual(true); const contents = tree.readContent(filePath); - expect(contents).toMatch(/\"outDir\": \"\.\.\/dist-server\"/); + expect(contents).toMatch('../../out-tsc/app-server'); }); it('should add dependency: @angular/platform-server', () => { @@ -72,47 +72,31 @@ describe('Universal Schematic', () => { expect(contents).toMatch(/\"@angular\/platform-server\": \"/); }); - it('should update .angular-cli.json with a server app', () => { + it('should update workspace with a server target', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/.angular-cli.json'; + const filePath = '/angular.json'; const contents = tree.readContent(filePath); - const config = JSON.parse(contents.toString()); - expect(config.apps.length).toEqual(2); - const app = config.apps[1]; - expect(app.platform).toEqual('server'); - expect(app.root).toEqual('src'); - expect(app.outDir).toEqual('dist-server'); - expect(app.index).toEqual('index.html'); - expect(app.main).toEqual('main.server.ts'); - expect(app.test).toEqual('test.ts'); - expect(app.tsconfig).toEqual('tsconfig.server.json'); - expect(app.testTsconfig).toEqual('tsconfig.spec.json'); - expect(app.environmentSource).toEqual('environments/environment.ts'); - expect(app.polyfills).not.toBeDefined(); + const arch = config.projects.bar.architect; + expect(arch.server).toBeDefined(); + expect(arch.server.builder).toBeDefined(); + const opts = arch.server.options; + expect(opts.outputPath).toEqual('dist/bar-server'); + expect(opts.main).toEqual('projects/bar/src/main.server.ts'); + expect(opts.tsConfig).toEqual('projects/bar/tsconfig.server.json'); }); it('should add a server transition to BrowerModule import', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/src/app/app.module.ts'; + const filePath = '/projects/bar/src/app/app.module.ts'; const contents = tree.readContent(filePath); expect(contents).toMatch(/BrowserModule\.withServerTransition\({ appId: 'serverApp' }\)/); }); it('should wrap the bootstrap call in a DOMContentLoaded event handler', () => { const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); - const filePath = '/src/main.ts'; + const filePath = '/projects/bar/src/main.ts'; const contents = tree.readContent(filePath); expect(contents).toMatch(/document.addEventListener\('DOMContentLoaded', \(\) => {/); }); - - it('should update .gitignore with the server outDir', () => { - const outDir = 'my-out-dir'; - const options = {...defaultOptions, outDir: outDir}; - const tree = schematicRunner.runSchematic('universal', options, appTree); - const filePath = '/.gitignore'; - const contents = tree.readContent(filePath); - - expect(contents).toMatch(outDir); - }); }); diff --git a/packages/schematics/angular/universal/schema.d.ts b/packages/schematics/angular/universal/schema.d.ts index 33b023c73f..119fae33ea 100644 --- a/packages/schematics/angular/universal/schema.d.ts +++ b/packages/schematics/angular/universal/schema.d.ts @@ -7,30 +7,14 @@ */ export interface Schema { - /** - * Name of the universal app - */ - name?: string; /** * Name or index of related client app. */ - clientApp?: string; + clientProject: string; /** * The appId to use withServerTransition. */ appId?: string; - /** - * The output directory for build results. - */ - outDir?: string; - /** - * The root directory of the app. - */ - root?: string; - /** - * Name of the index file - */ - index?: string; /** * The name of the main entry-point file. */ @@ -59,5 +43,4 @@ export interface Schema { * The name of the root module class. */ rootModuleClassName?: string; - sourceDir?: string; } diff --git a/packages/schematics/angular/universal/schema.json b/packages/schematics/angular/universal/schema.json index fa8f3528a8..e289ff3670 100644 --- a/packages/schematics/angular/universal/schema.json +++ b/packages/schematics/angular/universal/schema.json @@ -4,14 +4,9 @@ "title": "Angular Universal App Options Schema", "type": "object", "properties": { - "name": { + "clientProject": { "type": "string", - "description": "Name of the universal app." - }, - "clientApp": { - "type": "string", - "description": "Name or index of related client app.", - "default": "0" + "description": "Name of related client app." }, "appId": { "type": "string", @@ -19,24 +14,6 @@ "description": "The appId to use withServerTransition.", "default": "serverApp" }, - "outDir": { - "type": "string", - "format": "path", - "description": "The output directory for build results.", - "default": "dist-server" - }, - "root": { - "type": "string", - "format": "path", - "description": "The root directory of the app.", - "default": "src" - }, - "index": { - "type": "string", - "format": "path", - "description": "Name of the index file", - "default": "index.html" - }, "main": { "type": "string", "format": "path", @@ -75,15 +52,9 @@ "type": "string", "description": "The name of the root module class.", "default": "AppServerModule" - }, - "sourceDir": { - "type": "string", - "format": "path", - "description": "The path of the source directory.", - "default": "src", - "alias": "sd" } }, "required": [ + "clientProject" ] } diff --git a/packages/schematics/angular/utility/ast-utils.ts b/packages/schematics/angular/utility/ast-utils.ts index aa95071612..253c9692dc 100644 --- a/packages/schematics/angular/utility/ast-utils.ts +++ b/packages/schematics/angular/utility/ast-utils.ts @@ -233,6 +233,38 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); } +function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration|undefined { + if (ts.isClassDeclaration(node)) { + return node; + } + + return node.parent && findClassDeclarationParent(node.parent); +} + +/** + * Given a source file with @NgModule class(es), find the name of the first @NgModule class. + * + * @param source source file containing one or more @NgModule + * @returns the name of the first @NgModule, or `undefined` if none is found + */ +export function getFirstNgModuleName(source: ts.SourceFile): string|undefined { + // First, find the @NgModule decorators. + const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + if (ngModulesMetadata.length === 0) { + return undefined; + } + + // Then walk parent pointers up the AST, looking for the ClassDeclaration parent of the NgModule + // metadata. + const moduleClass = findClassDeclarationParent(ngModulesMetadata[0]); + if (!moduleClass || !moduleClass.name) { + return undefined; + } + + // Get the class name of the module ClassDeclaration. + return moduleClass.name.text; +} + export function addSymbolToNgModuleMetadata( source: ts.SourceFile, ngModulePath: string, diff --git a/packages/schematics/angular/utility/config.ts b/packages/schematics/angular/utility/config.ts index f80490dcd7..3add859083 100644 --- a/packages/schematics/angular/utility/config.ts +++ b/packages/schematics/angular/utility/config.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import { experimental } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; @@ -439,6 +440,27 @@ export interface CliConfig { }; } +export type WorkspaceSchema = experimental.workspace.WorkspaceSchema; + + +export function getWorkspacePath(host: Tree): string { + const possibleFiles = [ '/angular.json', '/.angular.json' ]; + const path = possibleFiles.filter(path => host.exists(path))[0]; + + return path; +} + +export function getWorkspace(host: Tree): WorkspaceSchema { + const path = getWorkspacePath(host); + const configBuffer = host.read(path); + if (configBuffer === null) { + throw new SchematicsException(`Could not find (${path})`); + } + const config = configBuffer.toString(); + + return JSON.parse(config); +} + export const configPath = '/.angular-cli.json'; export function getConfig(host: Tree): CliConfig { diff --git a/packages/schematics/angular/utility/find-module.ts b/packages/schematics/angular/utility/find-module.ts index c365515cde..cf55bbd2a8 100644 --- a/packages/schematics/angular/utility/find-module.ts +++ b/packages/schematics/angular/utility/find-module.ts @@ -13,10 +13,8 @@ export interface ModuleOptions { module?: string; name: string; flat?: boolean; - sourceDir?: string; path?: string; skipImport?: boolean; - appRoot?: string; } @@ -29,13 +27,13 @@ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path } if (!options.module) { - const pathToCheck = (options.sourceDir || '') + '/' + (options.path || '') + const pathToCheck = (options.path || '') + (options.flat ? '' : '/' + strings.dasherize(options.name)); return normalize(findModule(host, pathToCheck)); } else { const modulePath = normalize( - '/' + options.sourceDir + '/' + (options.appRoot || options.path) + '/' + options.module); + '/' + (options.path) + '/' + options.module); const moduleBaseName = normalize(modulePath).split('/').pop(); if (host.exists(modulePath)) { @@ -74,8 +72,8 @@ export function findModule(host: Tree, generateDir: string): Path { dir = dir.parent; } - throw new Error('Could not find an NgModule for the new component. Use the skip-import ' - + 'option to skip importing components in NgModule.'); + throw new Error('Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.'); } /** diff --git a/packages/schematics/angular/utility/find-module_spec.ts b/packages/schematics/angular/utility/find-module_spec.ts index 554d736258..8b92f4ebc5 100644 --- a/packages/schematics/angular/utility/find-module_spec.ts +++ b/packages/schematics/angular/utility/find-module_spec.ts @@ -5,8 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import { Path } from '@angular-devkit/core'; import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; +import { ModuleOptions, findModule, findModuleFromOptions } from './find-module'; describe('find-module', () => { @@ -62,4 +63,30 @@ describe('find-module', () => { } }); }); + + describe('findModuleFromOptions', () => { + let tree: Tree; + let options: ModuleOptions; + beforeEach(() => { + tree = new EmptyTree(); + options = { name: 'foo' }; + }); + + it('should find a module', () => { + tree.create('/projects/my-proj/src/app.module.ts', ''); + options.module = 'app.module.ts'; + options.path = '/projects/my-proj/src'; + const modPath = findModuleFromOptions(tree, options); + expect(modPath).toEqual('/projects/my-proj/src/app.module.ts' as Path); + }); + + it('should find a module in a sub dir', () => { + tree.create('/projects/my-proj/src/admin/foo.module.ts', ''); + options.name = 'other/test'; + options.module = 'admin/foo'; + options.path = '/projects/my-proj/src'; + const modPath = findModuleFromOptions(tree, options); + expect(modPath).toEqual('/projects/my-proj/src/admin/foo.module.ts' as Path); + }); + }); }); diff --git a/packages/schematics/angular/utility/latest-versions.ts b/packages/schematics/angular/utility/latest-versions.ts new file mode 100644 index 0000000000..bb03fdad13 --- /dev/null +++ b/packages/schematics/angular/utility/latest-versions.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export const latestVersions = { + // These versions should be kept up to date with latest Angular peer dependencies. + Angular: '^6.0.0-rc.1', + RxJs: '^6.0.0-rc.0', + ZoneJs: '^0.8.24', + TypeScript: '~2.7.2', + // The versions below must be manually updated when making a new devkit release. + DevkitBuildAngular: '~0.5.0', + DevkitBuildNgPackagr: '~0.5.0', +}; diff --git a/packages/schematics/angular/utility/ng-ast-utils.ts b/packages/schematics/angular/utility/ng-ast-utils.ts index 67cb3e619f..615b6e89da 100644 --- a/packages/schematics/angular/utility/ng-ast-utils.ts +++ b/packages/schematics/angular/utility/ng-ast-utils.ts @@ -7,9 +7,9 @@ */ import { normalize } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { dirname } from 'path'; import * as ts from 'typescript'; import { findNode, getSourceNodes } from '../utility/ast-utils'; -import { AppConfig } from '../utility/config'; export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { const mainBuffer = host.read(mainPath); @@ -75,10 +75,10 @@ export function findBootstrapModulePath(host: Tree, mainPath: string): string { return bootstrapModuleRelativePath; } -export function getAppModulePath(host: Tree, app: AppConfig) { - const mainPath = normalize(`/${app.root}/${app.main}`); +export function getAppModulePath(host: Tree, mainPath: string): string { const moduleRelativePath = findBootstrapModulePath(host, mainPath); - const modulePath = normalize(`/${app.root}/${moduleRelativePath}.ts`); + const mainDir = dirname(mainPath); + const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); return modulePath; } diff --git a/packages/schematics/angular/utility/parse-name.ts b/packages/schematics/angular/utility/parse-name.ts new file mode 100644 index 0000000000..e30c13d568 --- /dev/null +++ b/packages/schematics/angular/utility/parse-name.ts @@ -0,0 +1,25 @@ + +/** + * @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 { relative, Path } from "../../../angular_devkit/core/src/virtual-fs"; +import { Path, basename, dirname, normalize } from '@angular-devkit/core'; + +export interface Location { + name: string; + path: Path; +} + +export function parseName(path: string, name: string): Location { + const nameWithoutPath = basename(name as Path); + const namePath = dirname((path + '/' + name) as Path); + + return { + name: nameWithoutPath, + path: normalize('/' + namePath), + }; +} diff --git a/packages/schematics/angular/utility/parse-name_spec.ts b/packages/schematics/angular/utility/parse-name_spec.ts new file mode 100644 index 0000000000..30d92e87f5 --- /dev/null +++ b/packages/schematics/angular/utility/parse-name_spec.ts @@ -0,0 +1,39 @@ +/** + * @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 { parseName } from './parse-name'; + + +describe('parse-name', () => { + it('should handle just the name', () => { + const result = parseName('src/app', 'foo'); + expect(result.name).toEqual('foo'); + expect(result.path).toEqual('/src/app'); + }); + + it('should handle no path', () => { + const result = parseName('', 'foo'); + expect(result.name).toEqual('foo'); + expect(result.path).toEqual('/'); + }); + + it('should handle name has a path (sub-dir)', () => { + const result = parseName('src/app', 'bar/foo'); + expect(result.name).toEqual('foo'); + expect(result.path).toEqual('/src/app/bar'); + }); + + it('should handle name has a higher path', () => { + const result = parseName('src/app', '../foo'); + expect(result.name).toEqual('foo'); + expect(result.path).toEqual('/src'); + }); + + it('should handle name has a higher path above root', () => { + expect(() => parseName('src/app', '../../../foo')).toThrow(); + }); +}); diff --git a/packages/schematics/angular/utility/test/create-app-module.ts b/packages/schematics/angular/utility/test/create-app-module.ts index e7f764c9dd..5847d5cb10 100644 --- a/packages/schematics/angular/utility/test/create-app-module.ts +++ b/packages/schematics/angular/utility/test/create-app-module.ts @@ -5,10 +5,10 @@ * Use 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 { Tree } from '@angular-devkit/schematics'; +import { UnitTestTree } from '@angular-devkit/schematics/testing'; -export function createAppModule(tree: Tree, path?: string): Tree { +export function createAppModule(tree: UnitTestTree, path?: string): UnitTestTree { tree.create(path || '/src/app/app.module.ts', ` import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; diff --git a/packages/schematics/angular/utility/validation.ts b/packages/schematics/angular/utility/validation.ts new file mode 100644 index 0000000000..0578d18948 --- /dev/null +++ b/packages/schematics/angular/utility/validation.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { tags } from '@angular-devkit/core'; +import { SchematicsException } from '@angular-devkit/schematics'; + +export function validateName(name: string): void { + if (name && /^\d/.test(name)) { + throw new SchematicsException(tags.oneLine`name (${name}) + can not start with a digit.`); + } +} + +// Must start with a letter, and must contain only alphanumeric characters or dashes. +// When adding a dash the segment after the dash must also start with a letter. +export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; + +export function validateHtmlSelector(selector: string): void { + if (selector && !htmlSelectorRe.test(selector)) { + throw new SchematicsException(tags.oneLine`Selector (${selector}) + is invalid.`); + } +} diff --git a/packages/schematics/angular/application/files/README.md b/packages/schematics/angular/workspace/files/README.md similarity index 92% rename from packages/schematics/angular/application/files/README.md rename to packages/schematics/angular/workspace/files/README.md index ef84684d97..42a51babbb 100755 --- a/packages/schematics/angular/application/files/README.md +++ b/packages/schematics/angular/workspace/files/README.md @@ -12,7 +12,7 @@ Run `ng generate component component-name` to generate a new component. You can ## Build -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. ## Running unit tests diff --git a/packages/schematics/angular/application/files/__dot__editorconfig b/packages/schematics/angular/workspace/files/__dot__editorconfig similarity index 100% rename from packages/schematics/angular/application/files/__dot__editorconfig rename to packages/schematics/angular/workspace/files/__dot__editorconfig diff --git a/packages/schematics/angular/application/files/__dot__gitignore b/packages/schematics/angular/workspace/files/__dot__gitignore similarity index 92% rename from packages/schematics/angular/application/files/__dot__gitignore rename to packages/schematics/angular/workspace/files/__dot__gitignore index eabf65e51a..ee5c9d8336 100755 --- a/packages/schematics/angular/application/files/__dot__gitignore +++ b/packages/schematics/angular/workspace/files/__dot__gitignore @@ -2,7 +2,6 @@ # compiled output /dist -/dist-server /tmp /out-tsc @@ -35,10 +34,6 @@ yarn-error.log testem.log /typings -# e2e -/e2e/*.js -/e2e/*.map - # System Files .DS_Store Thumbs.db diff --git a/packages/schematics/angular/workspace/files/angular.json b/packages/schematics/angular/workspace/files/angular.json new file mode 100644 index 0000000000..5149ecd721 --- /dev/null +++ b/packages/schematics/angular/workspace/files/angular.json @@ -0,0 +1,6 @@ +{ + "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", + "version": 1, + "newProjectRoot": "<%= newProjectRoot %>", + "projects": {} +} \ No newline at end of file diff --git a/packages/schematics/angular/workspace/files/package.json b/packages/schematics/angular/workspace/files/package.json new file mode 100644 index 0000000000..4bda57221d --- /dev/null +++ b/packages/schematics/angular/workspace/files/package.json @@ -0,0 +1,48 @@ +{ + "name": "<%= utils.dasherize(name) %>", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "<%= latestVersions.Angular %>", + "@angular/common": "<%= latestVersions.Angular %>", + "@angular/compiler": "<%= latestVersions.Angular %>", + "@angular/core": "<%= latestVersions.Angular %>", + "@angular/forms": "<%= latestVersions.Angular %>", + "@angular/http": "<%= latestVersions.Angular %>", + "@angular/platform-browser": "<%= latestVersions.Angular %>", + "@angular/platform-browser-dynamic": "<%= latestVersions.Angular %>", + "@angular/router": "<%= latestVersions.Angular %>", + "core-js": "^2.5.4", + "rxjs": "<%= latestVersions.RxJs %>", + "zone.js": "<%= latestVersions.ZoneJs %>" + }, + "devDependencies": { + "@angular/cli": "~<%= version %>", + "@angular/compiler-cli": "<%= latestVersions.Angular %>", + "@angular/language-service": "<%= latestVersions.Angular %>", + "@types/jasmine": "~2.8.6", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.2.1", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~1.7.1", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~1.4.2", + "karma-jasmine": "~1.1.1", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.3.0", + "ts-node": "~5.0.1", + "tslint": "~5.9.1", + "typescript": "<%= latestVersions.TypeScript %>" + } +} diff --git a/packages/schematics/angular/workspace/files/tsconfig.json b/packages/schematics/angular/workspace/files/tsconfig.json new file mode 100644 index 0000000000..1162343d52 --- /dev/null +++ b/packages/schematics/angular/workspace/files/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ]<% if (strict) { %>, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "strictFunctionTypes": true<% } %> + } +} diff --git a/packages/schematics/angular/application/files/tslint.json b/packages/schematics/angular/workspace/files/tslint.json similarity index 92% rename from packages/schematics/angular/application/files/tslint.json rename to packages/schematics/angular/workspace/files/tslint.json index f28caa94d6..3ea984c776 100644 --- a/packages/schematics/angular/application/files/tslint.json +++ b/packages/schematics/angular/workspace/files/tslint.json @@ -18,7 +18,6 @@ "forin": true, "import-blacklist": [ true, - "rxjs", "rxjs/Rx" ], "import-spacing": true, @@ -117,18 +116,6 @@ "check-separator", "check-type" ], - "directive-selector": [ - true, - "attribute", - "<%= prefix %>", - "camelCase" - ], - "component-selector": [ - true, - "element", - "<%= prefix %>", - "kebab-case" - ], "no-output-on-prefix": true, "use-input-property-decorator": true, "use-output-property-decorator": true, diff --git a/packages/schematics/angular/workspace/index.ts b/packages/schematics/angular/workspace/index.ts new file mode 100644 index 0000000000..98f9aa0069 --- /dev/null +++ b/packages/schematics/angular/workspace/index.ts @@ -0,0 +1,33 @@ +/** + * @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 { strings } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + Tree, + apply, + mergeWith, + template, + url, +} from '@angular-devkit/schematics'; +import { latestVersions } from '../utility/latest-versions'; +import { Schema as WorkspaceOptions } from './schema'; + +export default function (options: WorkspaceOptions): Rule { + return (host: Tree, context: SchematicContext) => { + + return mergeWith(apply(url('./files'), [ + template({ + utils: strings, + ...options, + 'dot': '.', + latestVersions, + }), + ]))(host, context); + }; +} diff --git a/packages/schematics/angular/workspace/index_spec.ts b/packages/schematics/angular/workspace/index_spec.ts new file mode 100644 index 0000000000..669403396c --- /dev/null +++ b/packages/schematics/angular/workspace/index_spec.ts @@ -0,0 +1,58 @@ +/** + * @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 { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { latestVersions } from '../utility/latest-versions'; +import { Schema as WorkspaceOptions } from './schema'; + + +describe('Workspace Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: WorkspaceOptions = { + name: 'foo', + version: '6.0.0', + }; + + it('should create all files of a workspace', () => { + const options = { ...defaultOptions }; + + const tree = schematicRunner.runSchematic('workspace', options); + const files = tree.files; + expect(files.indexOf('/.editorconfig')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/angular.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/.gitignore')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/package.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/README.md')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/tsconfig.json')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/tslint.json')).toBeGreaterThanOrEqual(0); + }); + + it('should set the name in package.json', () => { + const tree = schematicRunner.runSchematic('workspace', defaultOptions); + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.name).toEqual('foo'); + }); + + it('should set the CLI version in package.json', () => { + const tree = schematicRunner.runSchematic('workspace', defaultOptions); + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@angular/cli']).toMatch('6.0.0'); + }); + + it('should use the latest known versions in package.json', () => { + const tree = schematicRunner.runSchematic('workspace', defaultOptions); + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.dependencies['@angular/core']).toEqual(latestVersions.Angular); + expect(pkg.dependencies['rxjs']).toEqual(latestVersions.RxJs); + expect(pkg.dependencies['zone.js']).toEqual(latestVersions.ZoneJs); + expect(pkg.devDependencies['typescript']).toEqual(latestVersions.TypeScript); + }); +}); diff --git a/packages/schematics/angular/workspace/schema.d.ts b/packages/schematics/angular/workspace/schema.d.ts new file mode 100644 index 0000000000..c07e14ca88 --- /dev/null +++ b/packages/schematics/angular/workspace/schema.d.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export interface Schema { + /** + * The name of the workspace. + */ + name: string; + /** + * The path where new projects will be created. + */ + newProjectRoot?: string; + /** + * Skip installing dependency packages. + */ + skipInstall?: boolean; + /** + * Link CLI to global version (internal development only). + */ + linkCli?: boolean; + /** + * Skip initializing a git repository. + */ + skipGit?: boolean; + /** + * Initial repository commit information. + */ + commit?: { name: string, email: string, message?: string }; + /** + * The version of the Angular CLI to use. + */ + version?: string; + /** + * Configure TypeScript in strict mode. + */ + strict?: boolean; +} diff --git a/packages/schematics/angular/workspace/schema.json b/packages/schematics/angular/workspace/schema.json new file mode 100644 index 0000000000..d7e642a340 --- /dev/null +++ b/packages/schematics/angular/workspace/schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularWorkspace", + "title": "Angular Workspace Options Schema", + "type": "object", + "properties": { + "name": { + "description": "The name of the workspace.", + "type": "string", + "format": "html-selector", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "newProjectRoot": { + "description": "The path where new projects will be created.", + "type": "string", + "visible": "false" + }, + "skipInstall": { + "description": "Skip installing dependency packages.", + "type": "boolean", + "default": false + }, + "linkCli": { + "description": "Link CLI to global version (internal development only).", + "type": "boolean", + "default": false, + "visible": false + }, + "skipGit": { + "description": "Skip initializing a git repository.", + "type": "boolean", + "default": false, + "alias": "g" + }, + "commit": { + "description": "Initial repository commit information.", + "default": null, + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "message": { + "type": "string" + } + }, + "required": [ + "name", + "email" + ] + } + ] + }, + "strict": { + "description": "Configure TypeScript in strict mode.", + "type": "boolean", + "default": false + }, + "version": { + "type": "string", + "description": "The version of the Angular CLI to use.", + "visible": false + } + }, + "required": [ + "name", + "version" + ] +} diff --git a/packages/schematics/package_update/package.json b/packages/schematics/package_update/package.json index 171028bb95..32b74ab808 100644 --- a/packages/schematics/package_update/package.json +++ b/packages/schematics/package_update/package.json @@ -13,12 +13,10 @@ }, "schematics": "./collection.json", "dependencies": { + "@angular-devkit/core": "0.0.0", + "@angular-devkit/schematics": "0.0.0", "semver": "^5.3.0", "semver-intersect": "^1.1.2", - "rxjs": "^5.5.6" - }, - "peerDependencies": { - "@angular-devkit/core": "0.0.0", - "@angular-devkit/schematics": "0.0.0" + "rxjs": "^6.0.0-beta.3" } -} +} \ No newline at end of file diff --git a/packages/schematics/package_update/utility/npm.ts b/packages/schematics/package_update/utility/npm.ts index 57b7bb3c2a..852e13f854 100644 --- a/packages/schematics/package_update/utility/npm.ts +++ b/packages/schematics/package_update/utility/npm.ts @@ -15,12 +15,15 @@ import { } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; import * as http from 'http'; -import { Observable } from 'rxjs/Observable'; -import { ReplaySubject } from 'rxjs/ReplaySubject'; -import { empty } from 'rxjs/observable/empty'; -import { from as observableFrom } from 'rxjs/observable/from'; -import { of as observableOf } from 'rxjs/observable/of'; -import { concat, ignoreElements, map, mergeMap } from 'rxjs/operators'; +import { + EMPTY, + Observable, + ReplaySubject, + concat, + from as observableFrom, + of as observableOf, +} from 'rxjs'; +import { ignoreElements, map, mergeMap } from 'rxjs/operators'; import * as semver from 'semver'; const semverIntersect = require('semver-intersect'); @@ -136,29 +139,29 @@ function _getRecursiveVersions( .filter(x => !!x), ); } else { - return empty(); + return EMPTY; } }), mergeMap(([depName, depVersion]: [string, string]) => { if (!packages[depName] || packages[depName] === depVersion) { - return empty(); + return EMPTY; } if (allVersions[depName] && semver.intersects(allVersions[depName], depVersion)) { allVersions[depName] = semverIntersect.intersect(allVersions[depName], depVersion); - return empty(); + return EMPTY; } return _getNpmPackageJson(depName, logger).pipe( - map(json => [packages[depName], depName, depVersion, json]), + map(json => ({ version: packages[depName], depName, depVersion, npmPackageJson: json })), ); }), - mergeMap(([version, depName, depVersion, npmPackageJson]) => { + mergeMap(({version, depName, depVersion, npmPackageJson}) => { const updateVersion = _getVersionFromNpmPackage(npmPackageJson, version, loose); const npmPackageVersions = Object.keys(npmPackageJson['versions'] as JsonObject); const match = semver.maxSatisfying(npmPackageVersions, updateVersion); if (!match) { - return empty(); + return EMPTY; } if (semver.lt( semverIntersect.parseRange(updateVersion).version, @@ -236,10 +239,11 @@ export function updatePackageJson( packages[name] = version; } - return _getRecursiveVersions(packageJson, packages, allVersions, context.logger, loose).pipe( - ignoreElements(), - concat(observableOf(tree)), - map(_ => tree), // Just to get the TypeScript typesystem fixed. + return concat( + _getRecursiveVersions(packageJson, packages, allVersions, context.logger, loose).pipe( + ignoreElements(), + ), + observableOf(tree), ); }, (tree: Tree) => { diff --git a/packages/schematics/schematics/blank/project-files/__dot__npmignore b/packages/schematics/schematics/blank/project-files/__dot__npmignore new file mode 100644 index 0000000000..c55ccfc3f5 --- /dev/null +++ b/packages/schematics/schematics/blank/project-files/__dot__npmignore @@ -0,0 +1,3 @@ +# Ignores TypeScript files, but keeps definitions. +*.ts +!*.d.ts diff --git a/packages/schematics/schematics/blank/project-files/tsconfig.json b/packages/schematics/schematics/blank/project-files/tsconfig.json index e93f82906a..0b296985db 100644 --- a/packages/schematics/schematics/blank/project-files/tsconfig.json +++ b/packages/schematics/schematics/blank/project-files/tsconfig.json @@ -5,6 +5,7 @@ "es2017", "dom" ], + "declaration": true, "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, diff --git a/packages/schematics/schematics/package.json b/packages/schematics/schematics/package.json index 7be96e5c32..0a9ec071fb 100644 --- a/packages/schematics/schematics/package.json +++ b/packages/schematics/schematics/package.json @@ -12,7 +12,7 @@ "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" }, "schematics": "./collection.json", - "peerDependencies": { + "dependencies": { "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0" } diff --git a/packages/schematics/update/collection.json b/packages/schematics/update/collection.json new file mode 100644 index 0000000000..fd168be8a3 --- /dev/null +++ b/packages/schematics/update/collection.json @@ -0,0 +1,15 @@ +{ + "schematics": { + "update": { + "factory": "./update", + "schema": "./update/schema.json", + "description": "Update one or multiple packages to versions, updating peer dependencies along the way." + }, + "migrate": { + "factory": "./migrate", + "schema": "./migrate/schema.json", + "description": "Schematic that calls the migrations of an installed package. Can be used separately", + "hidden": true + } + } +} diff --git a/packages/schematics/update/migrate/index.ts b/packages/schematics/update/migrate/index.ts new file mode 100644 index 0000000000..82849539d2 --- /dev/null +++ b/packages/schematics/update/migrate/index.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + chain, + externalSchematic, +} from '@angular-devkit/schematics'; +import * as semver from 'semver'; +import { PostUpdateSchema } from './schema'; + + +export default function(options: PostUpdateSchema): Rule { + return (tree: Tree, context: SchematicContext) => { + const schematicsToRun: { name: string; version: string; }[] = []; + + // Create the collection for the package. + const collection = context.engine.createCollection(options.collection); + for (const name of collection.listSchematicNames()) { + const schematic = collection.createSchematic(name, true); + + const description: JsonObject = schematic.description as JsonObject; + + if (typeof description['version'] == 'string') { + let version = description['version'] as string; + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}$/)) { + version += '.0'; + } + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}$/)) { + version += '.0'; + } + if (!semver.valid(version)) { + throw new SchematicsException( + `Invalid migration version: ${JSON.stringify(description['version'])}`, + ); + } + + if (semver.gt(version, options.from) && semver.lte(version, options.to)) { + schematicsToRun.push({ name, version }); + } + } + } + + schematicsToRun.sort((a, b) => { + const cmp = semver.compare(a.version, b.version); + + // Revert to comparing the names of the collection if the versions are equal. + return cmp == 0 ? a.name.localeCompare(b.name) : cmp; + }); + + if (schematicsToRun.length > 0) { + const rules = schematicsToRun.map(x => externalSchematic(options.collection, x.name, {})); + + return chain(rules)(tree, context); + } + + return tree; + }; +} diff --git a/packages/schematics/update/migrate/index_spec.ts b/packages/schematics/update/migrate/index_spec.ts new file mode 100644 index 0000000000..65a944d7ca --- /dev/null +++ b/packages/schematics/update/migrate/index_spec.ts @@ -0,0 +1,58 @@ +/** + * @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 { virtualFs } from '@angular-devkit/core'; +import { HostTree, VirtualTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { map } from 'rxjs/operators'; + + +describe('@schematics/update:migrate', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/update', __dirname + '/../collection.json', + ); + let host: virtualFs.test.TestHost; + let appTree: UnitTestTree = new UnitTestTree(new VirtualTree()); + + beforeEach(() => { + host = new virtualFs.test.TestHost({}); + appTree = new UnitTestTree(new HostTree(host)); + }); + + it('sorts and understand RC', done => { + // Since we cannot run tasks in unit tests, we need to validate that the default + // update schematic updates the package.json appropriately, AND validate that the + // migrate schematic actually do work appropriately, in a separate test. + schematicRunner.runSchematicAsync('migrate', { + package: 'test', + collection: __dirname + '/test/migration.json', + from: '1.0.0', + to: '2.0.0', + }, appTree).pipe( + map(tree => { + const resultJson = JSON.parse(tree.readContent('/migrations')); + + expect(resultJson).toEqual([ + 'migration-03', // "1.0.5" + 'migration-05', // "1.1.0-beta.0" + 'migration-04', // "1.1.0-beta.1" + 'migration-02', // "1.1.0" + 'migration-13', // "1.1.0" + 'migration-19', // "1.1" + 'migration-06', // "1.4.0" + 'migration-17', // "2.0.0-alpha" + 'migration-16', // "2.0.0-alpha.5" + 'migration-08', // "2.0.0-beta.0" + 'migration-07', // "2.0.0-rc.0" + 'migration-12', // "2.0.0-rc.4" + 'migration-14', // "2.0.0" + 'migration-20', // "2" + ]); + }), + ).subscribe(undefined, done.fail, done); + }); +}); diff --git a/packages/schematics/update/migrate/schema.json b/packages/schematics/update/migrate/schema.json new file mode 100644 index 0000000000..58fca362d0 --- /dev/null +++ b/packages/schematics/update/migrate/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "PostUpdateSchema", + "type": "object", + "properties": { + "package": { + "description": "The package to migrate.", + "type": "string" + }, + "collection": { + "description": "The collection to load the migrations from.", + "type": "string" + }, + "from": { + "description": "The version installed previously.", + "type": "string" + }, + "to": { + "description": "The version to migrate to.", + "type": "string" + } + }, + "required": ["package", "collection", "from", "to"] +} diff --git a/packages/schematics/update/migrate/schema.ts b/packages/schematics/update/migrate/schema.ts new file mode 100644 index 0000000000..400af572f4 --- /dev/null +++ b/packages/schematics/update/migrate/schema.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface PostUpdateSchema { + package: string; + collection: string; + from: string; + to: string; +} diff --git a/packages/schematics/update/migrate/test/migration.json b/packages/schematics/update/migrate/test/migration.json new file mode 100644 index 0000000000..22e886c078 --- /dev/null +++ b/packages/schematics/update/migrate/test/migration.json @@ -0,0 +1,25 @@ +{ + "schematics": { + "migration-00": { "version": "0.1.0", "factory": "./t1", "description": "." }, + "migration-01": { "version": "1.0.0", "factory": "./t1", "description": "." }, + "migration-02": { "version": "1.1.0", "factory": "./t1", "description": "." }, + "migration-03": { "version": "1.0.5", "factory": "./t1", "description": "." }, + "migration-04": { "version": "1.1.0-beta.1", "factory": "./t1", "description": "." }, + "migration-05": { "version": "1.1.0-beta.0", "factory": "./t1", "description": "." }, + "migration-06": { "version": "1.4.0", "factory": "./t1", "description": "." }, + "migration-07": { "version": "2.0.0-rc.0", "factory": "./t1", "description": "." }, + "migration-08": { "version": "2.0.0-beta.0", "factory": "./t1", "description": "." }, + "migration-09": { "version": "4.0.0", "factory": "./t1", "description": "." }, + "migration-10": { "version": "0.1.0", "factory": "./t1", "description": "." }, + "migration-11": { "version": "2.1.0", "factory": "./t1", "description": "." }, + "migration-12": { "version": "2.0.0-rc.4", "factory": "./t1", "description": "." }, + "migration-13": { "version": "1.1.0", "factory": "./t1", "description": "." }, + "migration-14": { "version": "2.0.0", "factory": "./t1", "description": "." }, + "migration-15": { "version": "2.0.1", "factory": "./t1", "description": "." }, + "migration-16": { "version": "2.0.0-alpha.5", "factory": "./t1", "description": "." }, + "migration-17": { "version": "2.0.0-alpha", "factory": "./t1", "description": "." }, + "migration-18": { "version": "1", "factory": "./t1", "description": "." }, + "migration-19": { "version": "1.1", "factory": "./t1", "description": "." }, + "migration-20": { "version": "2", "factory": "./t1", "description": "." } + } +} \ No newline at end of file diff --git a/packages/schematics/update/migrate/test/t1.ts b/packages/schematics/update/migrate/test/t1.ts new file mode 100644 index 0000000000..9122c812c2 --- /dev/null +++ b/packages/schematics/update/migrate/test/t1.ts @@ -0,0 +1,31 @@ +/** + * @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 { Rule, SchematicsException } from '@angular-devkit/schematics'; + +export default function(): Rule { + return (tree, context) => { + let content = tree.read('/migrations'); + + // Append the information to migration file. We then verify the order of execution. + if (!content) { + tree.create('/migrations', '[]'); + content = tree.read('/migrations'); + + if (!content) { + throw new SchematicsException(); + } + } + + const json = JSON.parse(content.toString('utf-8')); + json.push(context.schematic.description.name); + + tree.overwrite('/migrations', JSON.stringify(json)); + + return tree; + }; +} diff --git a/packages/schematics/update/package.json b/packages/schematics/update/package.json new file mode 100644 index 0000000000..d8f65c409a --- /dev/null +++ b/packages/schematics/update/package.json @@ -0,0 +1,22 @@ +{ + "name": "@schematics/update", + "version": "0.0.0", + "description": "Schematics specific to updating packages", + "keywords": [ + "blueprints", + "code generation", + "schematics", + "schematic" + ], + "scripts": { + "preinstall": "echo DO NOT INSTALL THIS PROJECT, ONLY THE ROOT PROJECT. && exit 1" + }, + "schematics": "./collection.json", + "dependencies": { + "@angular-devkit/core": "0.0.0", + "@angular-devkit/schematics": "0.0.0", + "semver": "^5.3.0", + "semver-intersect": "^1.1.2", + "rxjs": "^6.0.0-beta.3" + } +} \ No newline at end of file diff --git a/packages/schematics/update/update/index.ts b/packages/schematics/update/update/index.ts new file mode 100644 index 0000000000..9005f4b53c --- /dev/null +++ b/packages/schematics/update/update/index.ts @@ -0,0 +1,754 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging } from '@angular-devkit/core'; +import { + Rule, SchematicContext, SchematicsException, TaskId, + Tree, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask, RunSchematicTask } from '@angular-devkit/schematics/tasks'; +import { Observable, from as observableFrom, of } from 'rxjs'; +import { map, mergeMap, reduce, switchMap } from 'rxjs/operators'; +import * as semver from 'semver'; +import { getNpmPackageJson } from './npm'; +import { NpmRepositoryPackageJson } from './npm-package-json'; +import { JsonSchemaForNpmPackageJsonFiles } from './package-json'; +import { UpdateSchema } from './schema'; + +type VersionRange = string & { __: void; }; + +interface PackageVersionInfo { + version: VersionRange; + packageJson: JsonSchemaForNpmPackageJsonFiles; + updateMetadata: UpdateMetadata; +} + +interface PackageInfo { + name: string; + npmPackageJson: NpmRepositoryPackageJson; + installed: PackageVersionInfo; + target?: PackageVersionInfo; + packageJsonRange: string; +} + +interface UpdateMetadata { + packageGroup: string[]; + requirements: { [packageName: string]: string }; + migrations?: string; +} + +function _validateForwardPeerDependencies( + name: string, + infoMap: Map, + peers: {[name: string]: string}, + logger: logging.LoggerApi, +): boolean { + for (const [peer, range] of Object.entries(peers)) { + logger.debug(`Checking forward peer ${peer}...`); + const maybePeerInfo = infoMap.get(peer); + if (!maybePeerInfo) { + logger.error([ + `Package ${JSON.stringify(name)} has a missing peer dependency of`, + `${JSON.stringify(peer)} @ ${JSON.stringify(range)}.`, + ].join(' ')); + + return true; + } + + const peerVersion = maybePeerInfo.target && maybePeerInfo.target.packageJson.version + ? maybePeerInfo.target.packageJson.version + : maybePeerInfo.installed.version; + + logger.debug(` Range intersects(${range}, ${peerVersion})...`); + if (!semver.satisfies(peerVersion, range)) { + logger.error([ + `Package ${JSON.stringify(name)} has an incompatible peer dependency to`, + `${JSON.stringify(peer)} (requires ${JSON.stringify(range)},`, + `would install ${JSON.stringify(peerVersion)})`, + ].join(' ')); + + return true; + } + } + + return false; +} + + +function _validateReversePeerDependencies( + name: string, + version: string, + infoMap: Map, + logger: logging.LoggerApi, +) { + for (const [installed, installedInfo] of infoMap.entries()) { + const installedLogger = logger.createChild(installed); + installedLogger.debug(`${installed}...`); + const peers = (installedInfo.target || installedInfo.installed).packageJson.peerDependencies; + + for (const [peer, range] of Object.entries(peers || {})) { + if (peer != name) { + // Only check peers to the packages we're updating. We don't care about peers + // that are unmet but we have no effect on. + continue; + } + + if (!semver.satisfies(version, range)) { + logger.error([ + `Package ${JSON.stringify(installed)} has an incompatible peer dependency to`, + `${JSON.stringify(name)} (requires ${JSON.stringify(range)},`, + `would install ${JSON.stringify(version)}).`, + ].join(' ')); + + return true; + } + } + } + + return false; +} + +function _validateUpdatePackages( + infoMap: Map, + force: boolean, + logger: logging.LoggerApi, +): void { + logger.debug('Updating the following packages:'); + infoMap.forEach(info => { + if (info.target) { + logger.debug(` ${info.name} => ${info.target.version}`); + } + }); + + let peerErrors = false; + infoMap.forEach(info => { + const {name, target} = info; + if (!target) { + return; + } + + const pkgLogger = logger.createChild(name); + logger.debug(`${name}...`); + + const peers = target.packageJson.peerDependencies || {}; + peerErrors = _validateForwardPeerDependencies(name, infoMap, peers, pkgLogger) || peerErrors; + peerErrors + = _validateReversePeerDependencies(name, target.version, infoMap, pkgLogger) + || peerErrors; + }); + + if (!force && peerErrors) { + throw new SchematicsException(`Incompatible peer dependencies found. See above.`); + } +} + + +function _performUpdate( + tree: Tree, + context: SchematicContext, + infoMap: Map, + logger: logging.LoggerApi, + migrateOnly: boolean, +): Observable { + const packageJsonContent = tree.read('/package.json'); + if (!packageJsonContent) { + throw new SchematicsException('Could not find a package.json. Are you in a Node project?'); + } + + let packageJson: JsonSchemaForNpmPackageJsonFiles; + try { + packageJson = JSON.parse(packageJsonContent.toString()) as JsonSchemaForNpmPackageJsonFiles; + } catch (e) { + throw new SchematicsException('package.json could not be parsed: ' + e.message); + } + + const toInstall = [...infoMap.values()] + .map(x => [x.name, x.target, x.installed]) + // tslint:disable-next-line:non-null-operator + .filter(([name, target, installed]) => { + return !!name && !!target && !!installed; + }) as [string, PackageVersionInfo, PackageVersionInfo][]; + + toInstall.forEach(([name, target, installed]) => { + logger.info( + `Updating package.json with dependency ${name} ` + + `@ ${JSON.stringify(target.version)} (was ${JSON.stringify(installed.version)})...`, + ); + + if (packageJson.dependencies && packageJson.dependencies[name]) { + packageJson.dependencies[name] = target.version; + + if (packageJson.devDependencies && packageJson.devDependencies[name]) { + delete packageJson.devDependencies[name]; + } + if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.devDependencies && packageJson.devDependencies[name]) { + packageJson.devDependencies[name] = target.version; + + if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + delete packageJson.peerDependencies[name]; + } + } else if (packageJson.peerDependencies && packageJson.peerDependencies[name]) { + packageJson.peerDependencies[name] = target.version; + } else { + logger.warn(`Package ${name} was not found in dependencies.`); + } + }); + + const newContent = JSON.stringify(packageJson, null, 2); + if (packageJsonContent.toString() != newContent || migrateOnly) { + let installTask: TaskId[] = []; + if (!migrateOnly) { + // If something changed, also hook up the task. + tree.overwrite('/package.json', JSON.stringify(packageJson, null, 2)); + installTask = [context.addTask(new NodePackageInstallTask())]; + } + + // Run the migrate schematics with the list of packages to use. The collection contains + // version information and we need to do this post installation. Please note that the + // migration COULD fail and leave side effects on disk. + // Run the schematics task of those packages. + toInstall.forEach(([name, target, installed]) => { + if (!target.updateMetadata.migrations) { + return; + } + + const collection = ( + target.updateMetadata.migrations.match(/^[./]/) + ? name + '/' + : '' + ) + target.updateMetadata.migrations; + + context.addTask(new RunSchematicTask('@schematics/update', 'migrate', { + package: name, + collection, + from: installed.version, + to: target.version, + }), + installTask, + ); + }); + } + + return of(undefined); +} + +function _migrateOnly( + info: PackageInfo | undefined, + context: SchematicContext, + from: string, + to?: string, +) { + if (!info) { + return of(); + } + + const target = info.installed; + if (!target || !target.updateMetadata.migrations) { + return of(undefined); + } + + const collection = ( + target.updateMetadata.migrations.match(/^[./]/) + ? info.name + '/' + : '' + ) + target.updateMetadata.migrations; + + context.addTask(new RunSchematicTask('@schematics/update', 'migrate', { + package: info.name, + collection, + from: from, + to: to || target.version, + }), + ); + + return of(undefined); +} + +function _getUpdateMetadata( + packageJson: JsonSchemaForNpmPackageJsonFiles, + logger: logging.LoggerApi, +): UpdateMetadata { + const metadata = packageJson['ng-update']; + + const result: UpdateMetadata = { + packageGroup: [], + requirements: {}, + }; + + if (!metadata || typeof metadata != 'object' || Array.isArray(metadata)) { + return result; + } + + if (metadata['packageGroup']) { + const packageGroup = metadata['packageGroup']; + // Verify that packageGroup is an array of strings. This is not an error but we still warn + // the user and ignore the packageGroup keys. + if (!Array.isArray(packageGroup) || packageGroup.some(x => typeof x != 'string')) { + logger.warn( + `packageGroup metadata of package ${packageJson.name} is malformed. Ignoring.`, + ); + } else { + result.packageGroup = packageGroup; + } + } + + if (metadata['requirements']) { + const requirements = metadata['requirements']; + // Verify that requirements are + if (typeof requirements != 'object' + || Array.isArray(requirements) + || Object.keys(requirements).some(name => typeof requirements[name] != 'string')) { + logger.warn( + `requirements metadata of package ${packageJson.name} is malformed. Ignoring.`, + ); + } else { + result.requirements = requirements; + } + } + + if (metadata['migrations']) { + const migrations = metadata['migrations']; + if (typeof migrations != 'string') { + logger.warn(`migrations metadata of package ${packageJson.name} is malformed. Ignoring.`); + } else { + result.migrations = migrations; + } + } + + return result; +} + + +function _usageMessage( + options: UpdateSchema, + infoMap: Map, + logger: logging.LoggerApi, +) { + const packagesToUpdate = [...infoMap.entries()] + .sort() + .map(([name, info]) => { + const tag = options.next ? 'next' : 'latest'; + const version = info.npmPackageJson['dist-tags'][tag]; + const target = info.npmPackageJson.versions[version]; + + return [ + name, + info, + version, + target, + ] as [string, PackageInfo, string, JsonSchemaForNpmPackageJsonFiles]; + }) + .filter(([name, info, version, target]) => { + return (target && semver.compare(info.installed.version, version) < 0); + }) + .filter(([, , , target]) => { + return target['ng-update']; + }); + + if (packagesToUpdate.length == 0) { + logger.info('We analyzed your package.json and everything seems to be in order. Good work!'); + + return of(undefined); + } + + logger.info( + 'We analyzed your package.json, there are some packages to update:\n', + ); + + // Find the largest name to know the padding needed. + let namePad = Math.max(...[...infoMap.keys()].map(x => x.length)) + 2; + if (!Number.isFinite(namePad)) { + namePad = 30; + } + + logger.info( + ' ' + + 'Name'.padEnd(namePad) + + 'Version'.padEnd(25) + + ' Command to update', + ); + logger.info(' ' + '-'.repeat(namePad * 2 + 35)); + + packagesToUpdate.forEach(([name, info, version, target]) => { + let command = `npm install ${name}`; + if (target && target['ng-update']) { + // Show the ng command only when migrations are supported, otherwise it's a fancy + // npm install, really. + command = `ng update ${name}`; + } + + logger.info( + ' ' + + name.padEnd(namePad) + + `${info.installed.version} -> ${version}`.padEnd(25) + + ' ' + command, + ); + }); + + logger.info('\n'); + logger.info('There might be additional packages that are outdated.'); + logger.info('Or run ng update --all to try to update all at the same time.\n'); + + return of(undefined); +} + + +function _buildPackageInfo( + tree: Tree, + packages: Map, + allDependencies: Map, + npmPackageJson: NpmRepositoryPackageJson, + logger: logging.LoggerApi, +): PackageInfo { + const name = npmPackageJson.name; + const packageJsonRange = allDependencies.get(name); + if (!packageJsonRange) { + throw new SchematicsException( + `Package ${JSON.stringify(name)} was not found in package.json.`, + ); + } + + // Find out the currently installed version. Either from the package.json or the node_modules/ + // TODO: figure out a way to read package-lock.json and/or yarn.lock. + let installedVersion: string | undefined; + const packageContent = tree.read(`/node_modules/${name}/package.json`); + if (packageContent) { + const content = JSON.parse(packageContent.toString()) as JsonSchemaForNpmPackageJsonFiles; + installedVersion = content.version; + } + if (!installedVersion) { + // Find the version from NPM that fits the range to max. + installedVersion = semver.maxSatisfying( + Object.keys(npmPackageJson.versions), + packageJsonRange, + ); + } + + const installedPackageJson = npmPackageJson.versions[installedVersion] || packageContent; + if (!installedPackageJson) { + throw new SchematicsException( + `An unexpected error happened; package ${name} has no version ${installedVersion}.`, + ); + } + + let targetVersion: VersionRange | undefined = packages.get(name); + if (targetVersion) { + if (npmPackageJson['dist-tags'][targetVersion]) { + targetVersion = npmPackageJson['dist-tags'][targetVersion] as VersionRange; + } else { + targetVersion = semver.maxSatisfying( + Object.keys(npmPackageJson.versions), + targetVersion, + ) as VersionRange; + } + } + + if (targetVersion && semver.lte(targetVersion, installedVersion)) { + logger.debug(`Package ${name} already satisfied by package.json (${packageJsonRange}).`); + targetVersion = undefined; + } + + const target: PackageVersionInfo | undefined = targetVersion + ? { + version: targetVersion, + packageJson: npmPackageJson.versions[targetVersion], + updateMetadata: _getUpdateMetadata(npmPackageJson.versions[targetVersion], logger), + } + : undefined; + + // Check if there's an installed version. + return { + name, + npmPackageJson, + installed: { + version: installedVersion as VersionRange, + packageJson: installedPackageJson, + updateMetadata: _getUpdateMetadata(installedPackageJson, logger), + }, + target, + packageJsonRange, + }; +} + + +function _buildPackageList( + options: UpdateSchema, + projectDeps: Map, + logger: logging.LoggerApi, +): Map { + // Parse the packages options to set the targeted version. + const packages = new Map(); + const commandLinePackages = + (options.packages && options.packages.length > 0) + ? options.packages + : (options.all ? projectDeps.keys() : []); + + for (const pkg of commandLinePackages) { + // Split the version asked on command line. + const m = pkg.match(/^((?:@[^/]{1,100}\/)?[^@]{1,100})(?:@(.{1,100}))?$/); + if (!m) { + logger.warn(`Invalid package argument: ${JSON.stringify(pkg)}. Skipping.`); + continue; + } + + const [, npmName, maybeVersion] = m; + + const version = projectDeps.get(npmName); + if (!version) { + logger.warn(`Package not installed: ${JSON.stringify(npmName)}. Skipping.`); + continue; + } + + // Verify that people have an actual version in the package.json, otherwise (label or URL or + // gist or ...) we don't update it. + if ( + version.startsWith('http:') // HTTP + || version.startsWith('file:') // Local folder + || version.startsWith('git:') // GIT url + || version.match(/^\w{1,100}\/\w{1,100}/) // GitHub's "user/repo" + || version.match(/^(?:\.{0,2}\/)\w{1,100}/) // Local folder, maybe relative. + ) { + // We only do that for --all. Otherwise we have the installed version and the user specified + // it on the command line. + if (options.all) { + logger.warn( + `Package ${JSON.stringify(npmName)} has a custom version: ` + + `${JSON.stringify(version)}. Skipping.`, + ); + continue; + } + } + + packages.set(npmName, (maybeVersion || (options.next ? 'next' : 'latest')) as VersionRange); + } + + return packages; +} + + +function _addPackageGroup( + packages: Map, + allDependencies: ReadonlyMap, + npmPackageJson: NpmRepositoryPackageJson, + logger: logging.LoggerApi, +): void { + const maybePackage = packages.get(npmPackageJson.name); + if (!maybePackage) { + return; + } + + const version = npmPackageJson['dist-tags'][maybePackage] || maybePackage; + if (!npmPackageJson.versions[version]) { + return; + } + const ngUpdateMetadata = npmPackageJson.versions[version]['ng-update']; + if (!ngUpdateMetadata) { + return; + } + + const packageGroup = ngUpdateMetadata['packageGroup']; + if (!packageGroup) { + return; + } + if (!Array.isArray(packageGroup) || packageGroup.some(x => typeof x != 'string')) { + logger.warn(`packageGroup metadata of package ${npmPackageJson.name} is malformed.`); + + return; + } + + packageGroup + .filter(name => !packages.has(name)) // Don't override names from the command line. + .filter(name => allDependencies.has(name)) // Remove packages that aren't installed. + .forEach(name => { + packages.set(name, maybePackage); + }); +} + +/** + * Add peer dependencies of packages on the command line to the list of packages to update. + * We don't do verification of the versions here as this will be done by a later step (and can + * be ignored by the --force flag). + * @private + */ +function _addPeerDependencies( + packages: Map, + _allDependencies: ReadonlyMap, + npmPackageJson: NpmRepositoryPackageJson, + _logger: logging.LoggerApi, +): void { + const maybePackage = packages.get(npmPackageJson.name); + if (!maybePackage) { + return; + } + + const version = npmPackageJson['dist-tags'][maybePackage] || maybePackage; + if (!npmPackageJson.versions[version]) { + return; + } + + const packageJson = npmPackageJson.versions[version]; + const error = false; + + for (const [peer, range] of Object.entries(packageJson.peerDependencies || {})) { + if (!packages.has(peer)) { + packages.set(peer, range as VersionRange); + } + } + + if (error) { + throw new SchematicsException('An error occured, see above.'); + } +} + + +function _getAllDependencies(tree: Tree): Map { + const packageJsonContent = tree.read('/package.json'); + if (!packageJsonContent) { + throw new SchematicsException('Could not find a package.json. Are you in a Node project?'); + } + + let packageJson: JsonSchemaForNpmPackageJsonFiles; + try { + packageJson = JSON.parse(packageJsonContent.toString()) as JsonSchemaForNpmPackageJsonFiles; + } catch (e) { + throw new SchematicsException('package.json could not be parsed: ' + e.message); + } + + return new Map([ + ...Object.entries(packageJson.peerDependencies || {}), + ...Object.entries(packageJson.devDependencies || {}), + ...Object.entries(packageJson.dependencies || {}), + ] as [string, VersionRange][]); +} + +function _formatVersion(version: string | undefined) { + if (version === undefined) { + return undefined; + } + + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { + version += '.0'; + } + if (!version.match(/^\d{1,30}\.\d{1,30}\.\d{1,30}/)) { + version += '.0'; + } + if (!semver.valid(version)) { + throw new SchematicsException(`Invalid migration version: ${JSON.stringify(version)}`); + } + + return version; +} + + +export default function(options: UpdateSchema): Rule { + if (!options.packages) { + // We cannot just return this because we need to fetch the packages from NPM still for the + // help/guide to show. + options.packages = []; + } else if (typeof options.packages == 'string') { + // If a string, then we should split it and make it an array. + options.packages = options.packages.split(/,/g); + } + + if (options.migrateOnly && options.from) { + if (options.packages.length !== 1) { + throw new SchematicsException('--from requires that only a single package be passed.'); + } + } + + options.from = _formatVersion(options.from); + options.to = _formatVersion(options.to); + + return (tree: Tree, context: SchematicContext) => { + const logger = context.logger; + const allDependencies = _getAllDependencies(tree); + const packages = _buildPackageList(options, allDependencies, logger); + + return observableFrom([...allDependencies.keys()]).pipe( + // Grab all package.json from the npm repository. This requires a lot of HTTP calls so we + // try to parallelize as many as possible. + mergeMap(depName => getNpmPackageJson(depName, options.registry, logger)), + + // Build a map of all dependencies and their packageJson. + reduce>( + (acc, npmPackageJson) => { + // If the package was not found on the registry. It could be private, so we will just + // ignore. If the package was part of the list, we will error out, but will simply ignore + // if it's either not requested (so just part of package.json. silently) or if it's a + // `--all` situation. There is an edge case here where a public package peer depends on a + // private one, but it's rare enough. + if (!npmPackageJson.name) { + if (packages.has(npmPackageJson.requestedName)) { + if (options.all) { + logger.warn(`Package ${JSON.stringify(npmPackageJson.requestedName)} was not ` + + 'found on the registry. Skipping.'); + } else { + throw new SchematicsException( + `Package ${JSON.stringify(npmPackageJson.requestedName)} was not found on the ` + + 'registry. Cannot continue as this may be an error.'); + } + } + } else { + acc.set(npmPackageJson.name, npmPackageJson); + } + + return acc; + }, + new Map(), + ), + + map(npmPackageJsonMap => { + // Augment the command line package list with packageGroups and forward peer dependencies. + npmPackageJsonMap.forEach((npmPackageJson) => { + _addPackageGroup(packages, allDependencies, npmPackageJson, logger); + _addPeerDependencies(packages, allDependencies, npmPackageJson, logger); + }); + + // Build the PackageInfo for each module. + const packageInfoMap = new Map(); + npmPackageJsonMap.forEach((npmPackageJson) => { + packageInfoMap.set( + npmPackageJson.name, + _buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger), + ); + }); + + return packageInfoMap; + }), + + switchMap(infoMap => { + // Now that we have all the information, check the flags. + if (packages.size > 0) { + if (options.migrateOnly && options.from && options.packages) { + return _migrateOnly( + infoMap.get(options.packages[0]), + context, + options.from, + options.to, + ); + } + + const sublog = new logging.LevelCapLogger( + 'validation', + logger.createChild(''), + 'warn', + ); + _validateUpdatePackages(infoMap, options.force, sublog); + + return _performUpdate(tree, context, infoMap, logger, options.migrateOnly); + } else { + return _usageMessage(options, infoMap, logger); + } + }), + + switchMap(() => of(tree)), + ); + }; +} diff --git a/packages/schematics/update/update/index_spec.ts b/packages/schematics/update/update/index_spec.ts new file mode 100644 index 0000000000..8e7d42dd8c --- /dev/null +++ b/packages/schematics/update/update/index_spec.ts @@ -0,0 +1,197 @@ +/** + * @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 { normalize, virtualFs } from '@angular-devkit/core'; +import { HostTree, VirtualTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { map } from 'rxjs/operators'; + + +describe('@schematics/update', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/update', __dirname + '/../collection.json', + ); + let host: virtualFs.test.TestHost; + let appTree: UnitTestTree = new UnitTestTree(new VirtualTree()); + + beforeEach(() => { + host = new virtualFs.test.TestHost({ + '/package.json': `{ + "name": "blah", + "dependencies": { + "@angular-devkit-tests/update-base": "1.0.0" + } + }`, + }); + appTree = new UnitTestTree(new HostTree(host)); + }); + + it('updates package.json', done => { + // Since we cannot run tasks in unit tests, we need to validate that the default + // update schematic updates the package.json appropriately, AND validate that the + // migrate schematic actually do work appropriately, in a separate test. + schematicRunner.runSchematicAsync('update', { all: true }, appTree).pipe( + map(tree => { + const packageJson = JSON.parse(tree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-base']).toBe('1.1.0'); + + // Check install task. + expect(schematicRunner.tasks).toEqual([ + { + name: 'node-package', + options: jasmine.objectContaining({ + command: 'install', + }), + }, + ]); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('calls migration tasks', done => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.0.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + schematicRunner.runSchematicAsync('update', { all: true }, appTree).pipe( + map(tree => { + const packageJson = JSON.parse(tree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-base']).toBe('1.1.0'); + expect(packageJson['dependencies']['@angular-devkit-tests/update-migrations']) + .toBe('1.6.0'); + + // Check install task. + expect(schematicRunner.tasks).toEqual([ + { + name: 'node-package', + options: jasmine.objectContaining({ + command: 'install', + }), + }, + { + name: 'run-schematic', + options: jasmine.objectContaining({ + name: 'migrate', + }), + }, + ]); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('can migrate only', done => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.0.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + schematicRunner.runSchematicAsync('update', { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + }, appTree).pipe( + map(tree => { + const packageJson = JSON.parse(tree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-base']).toBe('1.0.0'); + expect(packageJson['dependencies']['@angular-devkit-tests/update-migrations']) + .toBe('1.0.0'); + + // Check install task. + expect(schematicRunner.tasks).toEqual([ + { + name: 'run-schematic', + options: jasmine.objectContaining({ + name: 'migrate', + }), + }, + ]); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('can migrate from only', done => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + schematicRunner.runSchematicAsync('update', { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + from: '0.1.2', + }, appTree).pipe( + map(tree => { + const packageJson = JSON.parse(tree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-migrations']) + .toBe('1.6.0'); + + // Check install task. + expect(schematicRunner.tasks).toEqual([ + { + name: 'run-schematic', + options: jasmine.objectContaining({ + name: 'migrate', + options: jasmine.objectContaining({ + from: '0.1.2', + to: '1.6.0', + }), + }), + }, + ]); + }), + ).subscribe(undefined, done.fail, done); + }); + + it('can install and migrate with --from (short version number)', done => { + // Add the basic migration package. + const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json'))); + const packageJson = JSON.parse(content); + packageJson['dependencies']['@angular-devkit-tests/update-migrations'] = '1.6.0'; + host.sync.write( + normalize('/package.json'), + virtualFs.stringToFileBuffer(JSON.stringify(packageJson)), + ); + + schematicRunner.runSchematicAsync('update', { + packages: ['@angular-devkit-tests/update-migrations'], + migrateOnly: true, + from: '0', + }, appTree).pipe( + map(tree => { + const packageJson = JSON.parse(tree.readContent('/package.json')); + expect(packageJson['dependencies']['@angular-devkit-tests/update-migrations']) + .toBe('1.6.0'); + + // Check install task. + expect(schematicRunner.tasks).toEqual([ + { + name: 'run-schematic', + options: jasmine.objectContaining({ + name: 'migrate', + options: jasmine.objectContaining({ + from: '0.0.0', + to: '1.6.0', + }), + }), + }, + ]); + }), + ).subscribe(undefined, done.fail, done); + }); +}); diff --git a/packages/schematics/update/update/npm-package-json.ts b/packages/schematics/update/update/npm-package-json.ts new file mode 100644 index 0000000000..675c45c0ff --- /dev/null +++ b/packages/schematics/update/update/npm-package-json.ts @@ -0,0 +1,27 @@ +/** + * @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 { JsonSchemaForNpmPackageJsonFiles } from './package-json'; + +export interface NpmRepositoryPackageJson { + name: string; + requestedName: string; + description: string; + + 'dist-tags': { + [name: string]: string; + }; + versions: { + [version: string]: JsonSchemaForNpmPackageJsonFiles; + }; + time: { + modified: string; + created: string; + + [version: string]: string; + }; +} diff --git a/packages/schematics/update/update/npm.ts b/packages/schematics/update/update/npm.ts new file mode 100644 index 0000000000..c7d12be3f8 --- /dev/null +++ b/packages/schematics/update/update/npm.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging } from '@angular-devkit/core'; +import * as http from 'http'; +import * as https from 'https'; +import { Observable, ReplaySubject } from 'rxjs'; +import * as url from 'url'; +import { NpmRepositoryPackageJson } from './npm-package-json'; + + +const npmPackageJsonCache = new Map>(); + + +/** + * Get the NPM repository's package.json for a package. This is p + * @param {string} packageName The package name to fetch. + * @param {string} registryUrl The NPM Registry URL to use. + * @param {LoggerApi} logger A logger instance to log debug information. + * @returns An observable that will put the pacakge.json content. + * @private + */ +export function getNpmPackageJson( + packageName: string, + registryUrl: string, + logger: logging.LoggerApi, +): Observable> { + let fullUrl = new url.URL(`http://${registryUrl}/${packageName.replace(/\//g, '%2F')}`); + try { + const registry = new url.URL(registryUrl); + registry.pathname = (registry.pathname || '') + .replace(/\/?$/, '/' + packageName.replace(/\//g, '%2F')); + fullUrl = new url.URL(url.format(registry)); + } catch (_) { + } + + logger.debug( + `Getting package.json from ${JSON.stringify(packageName)} (url: ${JSON.stringify(fullUrl)})...`, + ); + + let maybeRequest = npmPackageJsonCache.get(fullUrl.toString()); + if (!maybeRequest) { + const subject = new ReplaySubject(1); + + const protocolPackage = (fullUrl.protocol == 'https' ? https : http) as typeof http; + const request = protocolPackage.request(fullUrl, response => { + let data = ''; + response.on('data', chunk => data += chunk); + response.on('end', () => { + try { + const json = JSON.parse(data); + json.requestedName = packageName; + subject.next(json as NpmRepositoryPackageJson); + subject.complete(); + } catch (err) { + subject.error(err); + } + }); + response.on('error', err => subject.error(err)); + }); + request.end(); + + maybeRequest = subject.asObservable(); + npmPackageJsonCache.set(fullUrl.toString(), maybeRequest); + } + + return maybeRequest; +} diff --git a/packages/schematics/update/update/package-json.ts b/packages/schematics/update/update/package-json.ts new file mode 100644 index 0000000000..b4162f6fd2 --- /dev/null +++ b/packages/schematics/update/update/package-json.ts @@ -0,0 +1,264 @@ +/** + * @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 + */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ +// tslint:disable + +export type JsonSchemaForNpmPackageJsonFiles = CoreProperties & + JspmDefinition & + ( + | { + bundleDependencies?: BundledDependency; + [k: string]: any; + } + | { + bundledDependencies?: BundledDependency; + [k: string]: any; + }) & { + [k: string]: any; + }; +/** + * A person who has been involved in creating or maintaining this package + */ +export type Person = + | { + [k: string]: any; + } + | string; +/** + * Run AFTER the package is published + */ +export type ScriptsPublishAfter = string; +/** + * Run AFTER the package is installed + */ +export type ScriptsInstallAfter = string; +/** + * Run BEFORE the package is uninstalled + */ +export type ScriptsUninstallBefore = string; +/** + * Run BEFORE bump the package version + */ +export type ScriptsVersionBefore = string; +/** + * Run by the 'npm test' command + */ +export type ScriptsTest = string; +/** + * Run by the 'npm stop' command + */ +export type ScriptsStop = string; +/** + * Run by the 'npm start' command + */ +export type ScriptsStart = string; +/** + * Run by the 'npm restart' command. Note: 'npm restart' will run the stop and start scripts if no restart script is provided. + */ +export type ScriptsRestart = string; +/** + * Array of package names that will be bundled when publishing the package. + */ +export type BundledDependency = string[]; + +export interface CoreProperties { + /** + * The name of the package. + */ + name?: string; + /** + * Version must be parseable by node-semver, which is bundled with npm as a dependency. + */ + version?: string; + /** + * This helps people discover your package, as it's listed in 'npm search'. + */ + description?: string; + /** + * This helps people discover your package as it's listed in 'npm search'. + */ + keywords?: string[]; + /** + * The url to the project homepage. + */ + homepage?: string; + /** + * The url to your project's issue tracker and / or the email address to which issues should be reported. These are helpful for people who encounter issues with your package. + */ + bugs?: + | { + [k: string]: any; + } + | string; + /** + * You should specify a license for your package so that people know how they are permitted to use it, and any restrictions you're placing on it. + */ + license?: string; + /** + * You should specify a license for your package so that people know how they are permitted to use it, and any restrictions you're placing on it. + */ + licenses?: { + type?: string; + url?: string; + [k: string]: any; + }[]; + author?: Person; + /** + * A list of people who contributed to this package. + */ + contributors?: Person[]; + /** + * A list of people who maintains this package. + */ + maintainers?: Person[]; + /** + * The 'files' field is an array of files to include in your project. If you name a folder in the array, then it will also include the files inside that folder. + */ + files?: string[]; + /** + * The main field is a module ID that is the primary entry point to your program. + */ + main?: string; + bin?: + | string + | { + [k: string]: any; + }; + /** + * Specify either a single file or an array of filenames to put in place for the man program to find. + */ + man?: string[]; + directories?: { + /** + * If you specify a 'bin' directory, then all the files in that folder will be used as the 'bin' hash. + */ + bin?: string; + /** + * Put markdown files in here. Eventually, these will be displayed nicely, maybe, someday. + */ + doc?: string; + /** + * Put example scripts in here. Someday, it might be exposed in some clever way. + */ + example?: string; + /** + * Tell people where the bulk of your library is. Nothing special is done with the lib folder in any way, but it's useful meta info. + */ + lib?: string; + /** + * A folder that is full of man pages. Sugar to generate a 'man' array by walking the folder. + */ + man?: string; + test?: string; + [k: string]: any; + }; + /** + * Specify the place where your code lives. This is helpful for people who want to contribute. + */ + repository?: + | { + [k: string]: any; + } + | string; + /** + * The 'scripts' member is an object hash of script commands that are run at various times in the lifecycle of your package. The key is the lifecycle event, and the value is the command to run at that point. + */ + scripts?: { + /** + * Run BEFORE the package is published (Also run on local npm install without any arguments) + */ + prepublish?: string; + publish?: ScriptsPublishAfter; + postpublish?: ScriptsPublishAfter; + /** + * Run BEFORE the package is installed + */ + preinstall?: string; + install?: ScriptsInstallAfter; + postinstall?: ScriptsInstallAfter; + preuninstall?: ScriptsUninstallBefore; + uninstall?: ScriptsUninstallBefore; + /** + * Run AFTER the package is uninstalled + */ + postuninstall?: string; + preversion?: ScriptsVersionBefore; + version?: ScriptsVersionBefore; + /** + * Run AFTER bump the package version + */ + postversion?: string; + pretest?: ScriptsTest; + test?: ScriptsTest; + posttest?: ScriptsTest; + prestop?: ScriptsStop; + stop?: ScriptsStop; + poststop?: ScriptsStop; + prestart?: ScriptsStart; + start?: ScriptsStart; + poststart?: ScriptsStart; + prerestart?: ScriptsRestart; + restart?: ScriptsRestart; + postrestart?: ScriptsRestart; + [k: string]: string | undefined; + }; + /** + * A 'config' hash can be used to set configuration parameters used in package scripts that persist across upgrades. + */ + config?: { + [k: string]: any; + }; + dependencies?: Dependency; + devDependencies?: Dependency; + optionalDependencies?: Dependency; + peerDependencies?: Dependency; + engines?: { + [k: string]: string; + }; + engineStrict?: boolean; + /** + * You can specify which operating systems your module will run on + */ + os?: string[]; + /** + * If your code only runs on certain cpu architectures, you can specify which ones. + */ + cpu?: string[]; + /** + * If your package is primarily a command-line application that should be installed globally, then set this value to true to provide a warning if it is installed locally. + */ + preferGlobal?: boolean; + /** + * If set to true, then npm will refuse to publish it. + */ + private?: boolean; + publishConfig?: { + [k: string]: any; + }; + dist?: { + shasum?: string; + tarball?: string; + [k: string]: any; + }; + readme?: string; + [k: string]: any; +} +/** + * Dependencies are specified with a simple hash of package name to version range. The version range is a string which has one or more space-separated descriptors. Dependencies can also be identified with a tarball or git URL. + */ +export interface Dependency { + [k: string]: string; +} +export interface JspmDefinition { + jspm?: CoreProperties; + [k: string]: any; +} diff --git a/packages/schematics/update/update/schema.json b/packages/schematics/update/update/schema.json new file mode 100644 index 0000000000..621608cced --- /dev/null +++ b/packages/schematics/update/update/schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsUpdateSchema", + "title": "Schematic Options Schema", + "type": "object", + "properties": { + "packages": { + "description": "The packages to get.", + "type": "array", + "items": { + "type": "string" + }, + "$default": { + "$source": "argv" + } + }, + "force": { + "description": "If false, will error out if installed packages are incompatible with the update.", + "default": false, + "type": "boolean" + }, + "all": { + "description": "Whether to update all packages in package.json.", + "default": false, + "type": "boolean" + }, + "next": { + "description": "Use the largest version, including beta and RCs.", + "default": false, + "type": "boolean" + }, + "migrateOnly": { + "description": "Only perform a migration, does not update the installed version.", + "default": false, + "type": "boolean" + }, + "from": { + "description": "Version from which to migrate from. Only available with a single package being updated, and only on migration only.", + "type": "string" + }, + "to": { + "description": "Version up to which to apply migrations. Only available with a single package being updated, and only on migrations only. Requires from to be specified. Default to the installed version detected.", + "type": "string" + }, + "registry": { + "description": "The NPM registry to use. If you have an NPM proxy, you need to use this flag and set it to point to the proxy.", + "type": "string", + "oneOf": [ + { + "format": "uri" + }, + { + "format": "hostname" + } + ], + "default": "http://registry.npmjs.org/" + } + } +} diff --git a/packages/schematics/update/update/schema.ts b/packages/schematics/update/update/schema.ts new file mode 100644 index 0000000000..2b45fd81e3 --- /dev/null +++ b/packages/schematics/update/update/schema.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export interface UpdateSchema { + packages?: string | string[]; + force: boolean; + all: boolean; + next: boolean; + migrateOnly: boolean; + from?: string; + to?: string; + registry: string; +} diff --git a/rules/noGlobalTslintDisableRule.ts b/rules/noGlobalTslintDisableRule.ts index d5158a0fae..fea185ec79 100644 --- a/rules/noGlobalTslintDisableRule.ts +++ b/rules/noGlobalTslintDisableRule.ts @@ -44,7 +44,7 @@ class Walker extends Lint.RuleWalker { super.walk(sourceFile); // Ignore spec files. - if (sourceFile.fileName.match(/_spec.ts$/)) { + if (sourceFile.fileName.match(/_spec(_large)?.ts$/)) { return; } // Ignore benchmark files. diff --git a/scripts/build.ts b/scripts/build.ts index fd9aa0694a..126f637c4a 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -243,7 +243,7 @@ export default function(argv: { local?: boolean, snapshot?: boolean }, logger: l for (const packageName of sortedPackages) { specLogger.info(packageName); const pkg = packages[packageName]; - const files = glob.sync(path.join(pkg.dist, '**/*_spec.@(js|d.ts)')); + const files = glob.sync(path.join(pkg.dist, '**/*_spec?(_large).@(js|d.ts)')); specLogger.info(` ${files.length} spec files found...`); files.forEach(fileName => _rm(fileName)); } diff --git a/scripts/patch-dependencies.ts b/scripts/patch-dependencies.ts deleted file mode 100644 index b9a9b18c50..0000000000 --- a/scripts/patch-dependencies.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { execSync } from 'child_process'; -import { existsSync, writeFileSync } from 'fs'; - -const PATCH_LOCK = 'node_modules/rxjs/.patched'; - -export default function () { - if (!existsSync(PATCH_LOCK)) { - execSync('patch -p0 -i scripts/patches/rxjs-ts27.patch'); - execSync('patch -p0 -i scripts/patches/rxjs-typings.patch'); - writeFileSync(PATCH_LOCK, ''); - } -} diff --git a/scripts/patches/rxjs-ts27.patch b/scripts/patches/rxjs-ts27.patch deleted file mode 100644 index 99529ff3a5..0000000000 --- a/scripts/patches/rxjs-ts27.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- node_modules/rxjs/src/observable/dom/AjaxObservable.ts 2017-12-21 16:48:41.000000000 -0500 -+++ node_modules/rxjs/src/observable/dom/AjaxObservable.ts 2018-02-20 11:00:21.000000000 -0500 -@@ -462,13 +462,13 @@ - //IE does not support json as responseType, parse it internally - return xhr.responseType ? xhr.response : JSON.parse(xhr.response || xhr.responseText || 'null'); - } else { -- return JSON.parse(xhr.responseText || 'null'); -+ return JSON.parse((xhr as any).responseText || 'null'); - } - case 'xml': - return xhr.responseXML; - case 'text': - default: -- return ('response' in xhr) ? xhr.response : xhr.responseText; -+ return ('response' in xhr) ? xhr.response : (xhr as any).responseText; - } - } - diff --git a/scripts/patches/rxjs-typings.patch b/scripts/patches/rxjs-typings.patch deleted file mode 100644 index 19431748cf..0000000000 --- a/scripts/patches/rxjs-typings.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- node_modules/rxjs/Observable.d.ts 2018-02-20 11:24:56.000000000 -0500 -+++ node_modules/rxjs/Observable.d.ts 2018-02-20 11:25:21.000000000 -0500 -@@ -69,6 +69,7 @@ - pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction): Observable; - pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction, op8: OperatorFunction): Observable; - pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction, op4: OperatorFunction, op5: OperatorFunction, op6: OperatorFunction, op7: OperatorFunction, op8: OperatorFunction, op9: OperatorFunction): Observable; -+ pipe(...operations: OperatorFunction[]): Observable; - toPromise(this: Observable): Promise; - toPromise(this: Observable, PromiseCtor: typeof Promise): Promise; - toPromise(this: Observable, PromiseCtor: PromiseConstructorLike): Promise; diff --git a/scripts/release.ts b/scripts/release.ts index 6afc2179c8..21d3a13bde 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -26,7 +26,7 @@ function _showVersions(logger: logging.Logger) { const diff = pkg.dirty ? '!' : ''; const pad1 = ' '.slice(pkgName.length); - const pad2 = ' '.slice(version.length); + const pad2 = ' '.slice(version.length); const message = `${pkgName} ${pad1}v${version}${pad2}${hash} ${diff}`; if (pkg.private) { logger.debug(message); diff --git a/scripts/snapshots.ts b/scripts/snapshots.ts index 6cfbdb460e..2592317a85 100644 --- a/scripts/snapshots.ts +++ b/scripts/snapshots.ts @@ -33,10 +33,15 @@ function _copy(from: string, to: string) { function _exec(command: string, args: string[], opts: { cwd?: string }, logger: logging.Logger) { - const { status, error } = spawnSync(command, args, { ...opts }); + const { status, error, stderr } = spawnSync(command, args, { ...opts }); if (status != 0) { - logger.fatal(error.message); + logger.error(`Command failed: ${command} ${args.map(x => JSON.stringify(x)).join(', ')}`); + if (error) { + logger.error('Error: ' + (error ? error.message : 'undefined')); + } else { + logger.error(`STDERR:\n${stderr}`); + } throw error; } } @@ -92,6 +97,12 @@ export default function(opts: SnapshotsOptions, logger: logging.Logger) { _exec('git', ['clone', url], { cwd: root }, publishLogger); const destPath = path.join(root, path.basename(pkg.snapshotRepo)); + // Clear snapshot directory before publishing to remove deleted build files. + try { + _exec('git', ['rm', '-rf', './'], {cwd: destPath}, publishLogger); + } catch (e) { + // Ignore errors on delete. :shrug: + } _copy(pkg.dist, destPath); if (githubToken) { diff --git a/scripts/templates/readme.ejs b/scripts/templates/readme.ejs index dea310189c..8732ef3a83 100644 --- a/scripts/templates/readme.ejs +++ b/scripts/templates/readme.ejs @@ -78,8 +78,8 @@ for (const pkgName of Object.keys(packages)) { } %>**<%= mrPkg.name%>**<% - %> | [`<%= pkgName %>`](http://npmjs.com/packages/<%= pkgName %>)<% - %> | [![latest](https://img.shields.io/npm/v/<%= encode(pkgName) %>/latest.svg)](http://npmjs.com/packages/<%= pkgName %>)<% + %> | [`<%= pkgName %>`](https://npmjs.com/package/<%= pkgName %>)<% + %> | [![latest](https://img.shields.io/npm/v/<%= encode(pkgName) %>/latest.svg)](https://npmjs.com/package/<%= pkgName %>)<% %> | <% for (const link of mrPkg.links || []) { %>[![<%= link.label %>](https://img.shields.io/badge/<%= link.label %>--<%= link.color || 'green' %>.svg)](<%= link.url %>)<% } %> diff --git a/scripts/test.ts b/scripts/test.ts index 4a1ce971ac..e13537507a 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -142,6 +142,18 @@ if (process.argv.indexOf('--spec-reporter') != -1) { // Manually set exit code (needed with custom reporters) runner.onComplete((success: boolean) => { process.exitCode = success ? 0 : 1; + if (process.platform.startsWith('win')) { + // TODO(filipesilva): finish figuring out why this happens. + // We should not need to force exit here, but when: + // - on windows + // - running webpack-dev-server + // - with ngtools/webpack on the compilation + // Something seems to hang and the process never exists. + // This does not happen on linux, nor with webpack on watch mode. + // Until this is figured out, we need to exit the process manually after tests finish + // otherwise appveyor will hang until it timeouts. + process.exit(); + } }); @@ -152,10 +164,8 @@ glob.sync('packages/**/*.spec.ts') }); export default function (args: ParsedArgs, logger: logging.Logger) { - let regex = 'packages/**/*_spec.ts'; - if (args.glob) { - regex = `packages/**/${args.glob}/**/*_spec.ts`; - } + const specGlob = args.large ? '*_spec_large.ts' : '*_spec.ts'; + const regex = args.glob ? args.glob : `packages/**/${specGlob}`; if (args['code-coverage']) { runner.env.addReporter(new IstanbulReporter()); diff --git a/scripts/validate-commits.ts b/scripts/validate-commits.ts index aba664175f..2e3cd6bea0 100644 --- a/scripts/validate-commits.ts +++ b/scripts/validate-commits.ts @@ -125,7 +125,7 @@ export default function (argv: ValidateCommitsOptions, logger: logging.Logger) { _invalid(sha, message, 'should not have a scope'); continue; } - if (commits.length > 1) { + if (argv.ci && commits.length > 1) { _invalid(sha, message, 'release should always be alone in a PR'); continue; } diff --git a/scripts/validate-licenses.ts b/scripts/validate-licenses.ts index d47cdab9de..2e6e1815ab 100644 --- a/scripts/validate-licenses.ts +++ b/scripts/validate-licenses.ts @@ -70,6 +70,9 @@ const ignoredPackages = [ // so hard to manage. In talk with owner and users to switch over. 'uws@0.14.5', // TODO(filipesilva): remove this when karma is moved to e2e tests. + // TODO(filipesilva): remove this when spec_large is moved to e2e tests. + 'font-awesome@4.7.0', // (OFL-1.1 AND MIT) + ]; // Find all folders directly under a `node_modules` that have a package.json. diff --git a/tests/@angular_devkit/architect/angular-cli-workspace.schema.json b/tests/@angular_devkit/architect/angular-cli-workspace.schema.json deleted file mode 100644 index 4f60f41280..0000000000 --- a/tests/@angular_devkit/architect/angular-cli-workspace.schema.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "$schema": "../../../packages/angular_devkit/architect/src/workspace-schema.json", - "name": "pet-finder", - "version": 1, - "root": "src", - "defaultProject": "myApp", - "projects": { - "myApp": { - "projectType": "application", - "root": "src/my-app", - "defaultTarget": "browser", - "targets": { - "browser": { - "builder": "@angular-devkit/build-webpack:browser", - "options": { - "tsConfigPath": "tsconfig.app.json", - "outputPath": "dist", - "indexPath": "index.html", - "entryPoints": { - "main": "main.ts", - "polyfills": "polyfills.ts" - } - }, - "configurations": { - "production": { - "optimizationLevel": 1 - } - } - }, - "devServer": { - "builder": "@angular-devkit/build-webpack:devServer", - "options": { - "browserTarget": "myApp:browser" - } - }, - "extractI18n": { - "builder": "@angular-devkit/build-webpack:extractI18n", - "options": { - "browserTarget": "myApp:browser" - } - }, - "karma": { - "builder": "@angular-devkit/build-webpack:karma", - "options": { - "tsConfigPath": "tsconfig.app.json", - "karmaConfigPath": "karma.conf.js", - "entryPoints": { - "main": "main.ts", - "polyfills": "polyfills.ts" - } - } - }, - "tslint": { - "builder": "@angular-devkit/build-webpack:tslint", - "options": { - "tsConfigPath": "tsconfig.app.json" - } - } - } - }, - "myLibrary": { - "projectType": "library", - "root": "./src/library", - "targets": { - "nodeModule": { - "builder": "@angular-devkit/build-ng-packagr:nodeModule", - "options": { - "tsConfigPath": "tsconfig.json", - "packageJsonPath": "package.json", - "entryPoints": { - "index": "index.ts" - } - } - } - } - }, - "e2e": { - "projectType": "application", - "root": "./e2e", - "defaultTarget": "browser", - "targets": { - "browser": { - "builder": "@angular-devkit/build-webpack:protractor", - "options": { - "protractorConfigPath": "protractor.conf.js", - "devServerTarget": "myApp:browser" - } - } - } - } - } -} diff --git a/tests/@angular_devkit/build_angular/hello-world-app/.angular.json b/tests/@angular_devkit/build_angular/hello-world-app/.angular.json new file mode 100644 index 0000000000..c3c338ff3e --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/.angular.json @@ -0,0 +1,168 @@ +{ + "$schema": "../../../../packages/angular_devkit/core/src/workspace/workspace-schema.json", + "version": 1, + "newProjectRoot": "./projects", + "cli": {}, + "schematics": {}, + "architect": {}, + "projects": { + "app": { + "root": "src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "../../../../packages/angular_devkit/build_angular:browser", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "progress": false, + "assets": [ + { + "glob": "favicon.ico", + "input": "src/", + "output": "/" + }, + { + "glob": "**/*", + "input": "src/assets", + "output": "/assets" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environments.ts", + "with": "src/environments/environments.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "server": { + "builder": "../../../../packages/angular_devkit/build_angular:server", + "options": { + "outputPath": "dist-server", + "main": "src/main.server.ts", + "tsConfig": "src/tsconfig.server.json" + } + }, + "app-shell": { + "builder": "../../../../packages/angular_devkit/build_angular:app-shell", + "options": { + "browserTarget": "app:build", + "serverTarget": "app:server" + } + }, + "serve": { + "builder": "../../../../packages/angular_devkit/build_angular:dev-server", + "options": { + "browserTarget": "app:build", + "watch": false + }, + "configurations": { + "production": { + "browserTarget": "app:build:production" + } + } + }, + "extract-i18n": { + "builder": "../../../../packages/angular_devkit/build_angular:extract-i18n", + "options": { + "browserTarget": "app:build" + } + }, + "test": { + "builder": "../../../../packages/angular_devkit/build_angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "browsers": "ChromeHeadless", + "progress": false, + "watch": false, + "styles": [ + { + "input": "src/styles.css" + } + ], + "scripts": [], + "assets": [ + { + "glob": "favicon.ico", + "input": "src/", + "output": "/" + }, + { + "glob": "**/*", + "input": "src/assets", + "output": "/assets" + } + ] + } + }, + "lint": { + "builder": "../../../../packages/angular_devkit/build_angular:tslint", + "options": { + "tsConfig": "src/tsconfig.app.json", + "exclude": [ + "**/node_modules/**" + ] + } + }, + "lint-test": { + "builder": "../../../../packages/angular_devkit/build_angular:tslint", + "options": { + "tsConfig": "src/tsconfig.spec.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "app-e2e": { + "root": "e2e", + "projectType": "application", + "architect": { + "e2e": { + "builder": "../../../../packages/angular_devkit/build_angular:protractor", + "options": { + "protractorConfig": "protractor.conf.js", + "devServerTarget": "app:serve", + "webdriverUpdate": false + } + }, + "lint": { + "builder": "../../../../packages/angular_devkit/build_angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + } +} diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/.editorconfig b/tests/@angular_devkit/build_angular/hello-world-app/.editorconfig similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/.editorconfig rename to tests/@angular_devkit/build_angular/hello-world-app/.editorconfig diff --git a/tests/@angular_devkit/build_angular/hello-world-app/.gitignore b/tests/@angular_devkit/build_angular/hello-world-app/.gitignore new file mode 100644 index 0000000000..e3038ca2d3 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/.gitignore @@ -0,0 +1,3 @@ +# Don't ignore node_modules, this project is not meant to be installed. +# Also, ~ import path in styles does only looks in the first node_modules found. +# /node_modules diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/README.md b/tests/@angular_devkit/build_angular/hello-world-app/README.md similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/README.md rename to tests/@angular_devkit/build_angular/hello-world-app/README.md diff --git a/tests/@angular_devkit/build_angular/hello-world-app/browserslist b/tests/@angular_devkit/build_angular/hello-world-app/browserslist new file mode 100644 index 0000000000..1b1be90441 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/browserslist @@ -0,0 +1,5 @@ +> 0.5% +last 2 versions +Firefox ESR +not dead +IE 9-11 \ No newline at end of file diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/e2e/app.e2e-spec.ts b/tests/@angular_devkit/build_angular/hello-world-app/e2e/app.e2e-spec.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/e2e/app.e2e-spec.ts rename to tests/@angular_devkit/build_angular/hello-world-app/e2e/app.e2e-spec.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/e2e/app.po.ts b/tests/@angular_devkit/build_angular/hello-world-app/e2e/app.po.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/e2e/app.po.ts rename to tests/@angular_devkit/build_angular/hello-world-app/e2e/app.po.ts diff --git a/packages/schematics/angular/application/files/e2e/tsconfig.e2e.json b/tests/@angular_devkit/build_angular/hello-world-app/e2e/tsconfig.e2e.json similarity index 91% rename from packages/schematics/angular/application/files/e2e/tsconfig.e2e.json rename to tests/@angular_devkit/build_angular/hello-world-app/e2e/tsconfig.e2e.json index 1d9e5edf09..39b800f789 100644 --- a/packages/schematics/angular/application/files/e2e/tsconfig.e2e.json +++ b/tests/@angular_devkit/build_angular/hello-world-app/e2e/tsconfig.e2e.json @@ -2,7 +2,6 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/e2e", - "baseUrl": "./", "module": "commonjs", "target": "es5", "types": [ diff --git a/packages/schematics/angular/application/files/karma.conf.js b/tests/@angular_devkit/build_angular/hello-world-app/karma.conf.js similarity index 80% rename from packages/schematics/angular/application/files/karma.conf.js rename to tests/@angular_devkit/build_angular/hello-world-app/karma.conf.js index af139fada3..3aaab1c2f0 100644 --- a/packages/schematics/angular/application/files/karma.conf.js +++ b/tests/@angular_devkit/build_angular/hello-world-app/karma.conf.js @@ -1,21 +1,24 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const path = require('path'); + module.exports = function (config) { config.set({ basePath: '', - frameworks: ['jasmine', '@angular/cli'], + frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), - require('@angular/cli/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma') ], client:{ clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { + dir: path.join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true }, diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/package.json b/tests/@angular_devkit/build_angular/hello-world-app/package.json similarity index 64% rename from tests/@angular_devkit/build_webpack/hello-world-app/package.json rename to tests/@angular_devkit/build_angular/hello-world-app/package.json index a09e45fde5..1c11bcc465 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/package.json +++ b/tests/@angular_devkit/build_angular/hello-world-app/package.json @@ -12,23 +12,23 @@ }, "private": true, "dependencies": { - "@angular/animations": "^5.2.0", - "@angular/common": "^5.2.0", - "@angular/compiler": "^5.2.0", - "@angular/core": "^5.2.0", - "@angular/forms": "^5.2.0", - "@angular/http": "^5.2.0", - "@angular/platform-browser": "^5.2.0", - "@angular/platform-browser-dynamic": "^5.2.0", - "@angular/router": "^5.2.0", + "@angular/animations": "^6.0.0-rc.0", + "@angular/common": "^6.0.0-rc.0", + "@angular/compiler": "^6.0.0-rc.0", + "@angular/core": "^6.0.0-rc.0", + "@angular/forms": "^6.0.0-rc.0", + "@angular/http": "^6.0.0-rc.0", + "@angular/platform-browser": "^6.0.0-rc.0", + "@angular/platform-browser-dynamic": "^6.0.0-rc.0", + "@angular/router": "^6.0.0-rc.0", "core-js": "^2.4.1", - "rxjs": "^5.5.6", + "rxjs": "^6.0.0-beta.3", "zone.js": "^0.8.19" }, "devDependencies": { "@angular/cli": "1.7.0-beta.1", - "@angular/compiler-cli": "^5.2.0", - "@angular/language-service": "^5.2.0", + "@angular/compiler-cli": "^6.0.0-rc.0", + "@angular/language-service": "^6.0.0-rc.0", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/protractor.conf.js b/tests/@angular_devkit/build_angular/hello-world-app/protractor.conf.js similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/protractor.conf.js rename to tests/@angular_devkit/build_angular/hello-world-app/protractor.conf.js diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.css b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.css similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.css rename to tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.css diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.html b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.html similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.html rename to tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.html diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.spec.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.spec.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.spec.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.spec.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.component.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/app/app.component.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.module.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.module.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/app/app.module.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/app/app.module.ts diff --git a/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.server.module.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.server.module.ts new file mode 100644 index 0000000000..795380cd22 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/app/app.server.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ], + bootstrap: [AppComponent], +}) +export class AppServerModule {} diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/assets/.gitkeep b/tests/@angular_devkit/build_angular/hello-world-app/src/assets/.gitkeep similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/assets/.gitkeep rename to tests/@angular_devkit/build_angular/hello-world-app/src/assets/.gitkeep diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/environments/environment.prod.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/environments/environment.prod.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/environments/environment.prod.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/environments/environment.prod.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/environments/environment.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/environments/environment.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/environments/environment.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/environments/environment.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/favicon.ico b/tests/@angular_devkit/build_angular/hello-world-app/src/favicon.ico similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/favicon.ico rename to tests/@angular_devkit/build_angular/hello-world-app/src/favicon.ico diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/index.html b/tests/@angular_devkit/build_angular/hello-world-app/src/index.html similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/index.html rename to tests/@angular_devkit/build_angular/hello-world-app/src/index.html diff --git a/tests/@angular_devkit/build_angular/hello-world-app/src/main.server.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/main.server.ts new file mode 100644 index 0000000000..b9ca5050c2 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/main.server.ts @@ -0,0 +1,9 @@ +import { enableProdMode } from '@angular/core'; + +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +export { AppServerModule } from './app/app.server.module'; diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/main.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/main.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/main.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/main.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/polyfills.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/polyfills.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/polyfills.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/polyfills.ts diff --git a/tests/@angular_devkit/build_angular/hello-world-app/src/spectrum.png b/tests/@angular_devkit/build_angular/hello-world-app/src/spectrum.png new file mode 100644 index 0000000000..2a5f123afc Binary files /dev/null and b/tests/@angular_devkit/build_angular/hello-world-app/src/spectrum.png differ diff --git a/tests/@angular_devkit/build_angular/hello-world-app/src/src/locale/messages.xlf b/tests/@angular_devkit/build_angular/hello-world-app/src/src/locale/messages.xlf new file mode 100644 index 0000000000..5ba84a9b35 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/src/locale/messages.xlf @@ -0,0 +1,18 @@ + + + + + + i18n test + + app/app.component.ts + 21 + + + app/app.component.ts + 23 + + + + + diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/styles.css b/tests/@angular_devkit/build_angular/hello-world-app/src/styles.css similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/styles.css rename to tests/@angular_devkit/build_angular/hello-world-app/src/styles.css diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/test.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/test.ts similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/test.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/test.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.app.json b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.app.json similarity index 90% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.app.json rename to tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.app.json index 39ba8dbacb..b90ffb0148 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.app.json +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.app.json @@ -2,7 +2,6 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", - "baseUrl": "./", "module": "es2015", "types": [] }, diff --git a/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.server.json b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.server.json new file mode 100644 index 0000000000..36fa0af8d3 --- /dev/null +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.server.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../dist-server", + "baseUrl": "./", + "module": "commonjs", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ], + "angularCompilerOptions": { + "entryModule": "app/app.server.module#AppServerModule" + } +} diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.spec.json b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.spec.json similarity index 87% rename from tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.spec.json rename to tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.spec.json index ac22a298ac..8f7cedecab 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/src/tsconfig.spec.json +++ b/tests/@angular_devkit/build_angular/hello-world-app/src/tsconfig.spec.json @@ -2,7 +2,6 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/spec", - "baseUrl": "./", "module": "commonjs", "types": [ "jasmine", @@ -10,7 +9,8 @@ ] }, "files": [ - "test.ts" + "test.ts", + "polyfills.ts" ], "include": [ "**/*.spec.ts", diff --git a/packages/schematics/angular/application/files/__sourcedir__/typings.d.ts b/tests/@angular_devkit/build_angular/hello-world-app/src/typings.d.ts similarity index 100% rename from packages/schematics/angular/application/files/__sourcedir__/typings.d.ts rename to tests/@angular_devkit/build_angular/hello-world-app/src/typings.d.ts diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/tsconfig.json b/tests/@angular_devkit/build_angular/hello-world-app/tsconfig.json similarity index 94% rename from tests/@angular_devkit/build_webpack/hello-world-app/tsconfig.json rename to tests/@angular_devkit/build_angular/hello-world-app/tsconfig.json index a6c016bf38..ef44e2862b 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/tsconfig.json +++ b/tests/@angular_devkit/build_angular/hello-world-app/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/tslint.json b/tests/@angular_devkit/build_angular/hello-world-app/tslint.json similarity index 100% rename from tests/@angular_devkit/build_webpack/hello-world-app/tslint.json rename to tests/@angular_devkit/build_angular/hello-world-app/tslint.json diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/.gitignore b/tests/@angular_devkit/build_ng_packagr/ng-packaged/.gitignore similarity index 94% rename from tests/@angular_devkit/build_webpack/hello-world-app/.gitignore rename to tests/@angular_devkit/build_ng_packagr/ng-packaged/.gitignore index d0448e8d4e..54bfd2001e 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/.gitignore +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/.gitignore @@ -2,7 +2,6 @@ # compiled output /dist -/dist-server /tmp /out-tsc @@ -41,5 +40,3 @@ testem.log # System Files .DS_Store Thumbs.db - -src/messages.xlf diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/angular.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/angular.json new file mode 100644 index 0000000000..f316e25c26 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/angular.json @@ -0,0 +1,41 @@ +{ + "$schema": "../../../../packages/angular_devkit/core/src/workspace/workspace-schema.json", + "version": 1, + "projects": { + "lib": { + "root": "projects/lib", + "projectType": "library", + "architect": { + "build": { + "builder": "../../../../packages/angular_devkit/build_ng_packagr:build", + "options": { + "project": "projects/lib/ng-package.json" + } + }, + "test": { + "builder": "../../../../packages/angular_devkit/build_angular:karma", + "options": { + "main": "projects/lib/src/test.ts", + "tsConfig": "projects/lib/tsconfig.spec.json", + "karmaConfig": "projects/lib/karma.conf.js", + "browsers": "ChromeHeadless", + "progress": false, + "watch": false + } + }, + "lint": { + "builder": "../../../../packages/angular_devkit/build_angular:tslint", + "options": { + "tsConfig": [ + "projects/lib/tsconfig.lint.json", + "projects/lib/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/karma.conf.js b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/karma.conf.js new file mode 100644 index 0000000000..d5fd42ec88 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/karma.conf.js @@ -0,0 +1,34 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, 'coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + angularCli: { + environment: 'dev' + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/ng-package.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/ng-package.json new file mode 100644 index 0000000000..36fcf49d9c --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/lib", + "lib": { + "entryFile": "src/public_api.ts" + } +} \ No newline at end of file diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/package.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/package.json new file mode 100644 index 0000000000..6c4cf3cf93 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "lib", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^6.0.0-rc.0 || ^6.0.0", + "@angular/core": "^6.0.0-rc.0 || ^6.0.0" + } +} \ No newline at end of file diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.spec.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.spec.ts new file mode 100644 index 0000000000..c02003e11d --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LibComponent } from './lib.component'; + +describe('LibComponent', () => { + let component: LibComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LibComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LibComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.ts new file mode 100644 index 0000000000..4c25073a25 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'lib', + template: ` +

+ lib works! +

+ `, + styles: [] +}) +export class LibComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.module.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.module.ts new file mode 100644 index 0000000000..511e052093 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { LibComponent } from './lib.component'; +import { LibService } from './lib.service'; + +@NgModule({ + imports: [ + ], + declarations: [LibComponent], + providers: [LibService] +}) +export class LibModule { } diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.spec.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.spec.ts new file mode 100644 index 0000000000..a62c02c2fb --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { LibService } from './lib.service'; + +describe('LibService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [LibService] + }); + }); + + it('should be created', inject([LibService], (service: LibService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.ts new file mode 100644 index 0000000000..3e6c7e592b --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/lib/lib.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class LibService { + + constructor() { } + +} diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/public_api.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/public_api.ts new file mode 100644 index 0000000000..a09e1deb35 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/public_api.ts @@ -0,0 +1,7 @@ +/* + * Public API Surface of lib + */ + +export * from './lib/lib.service'; +export * from './lib/lib.component'; +export * from './lib/lib.module'; diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/test.ts b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/test.ts new file mode 100644 index 0000000000..e11ff1c97b --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/src/test.ts @@ -0,0 +1,22 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'core-js/es7/reflect'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.lint.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.lint.json new file mode 100644 index 0000000000..f098e3ea6a --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.lint.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.spec.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.spec.json new file mode 100644 index 0000000000..16da33db07 --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/packages/schematics/angular/application/files/tsconfig.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/tsconfig.json similarity index 94% rename from packages/schematics/angular/application/files/tsconfig.json rename to tests/@angular_devkit/build_ng_packagr/ng-packaged/tsconfig.json index a6c016bf38..ef44e2862b 100644 --- a/packages/schematics/angular/application/files/tsconfig.json +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/tsconfig.json @@ -1,6 +1,7 @@ { "compileOnSave": false, "compilerOptions": { + "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, diff --git a/tests/@angular_devkit/build_ng_packagr/ng-packaged/tslint.json b/tests/@angular_devkit/build_ng_packagr/ng-packaged/tslint.json new file mode 100644 index 0000000000..944ad565fc --- /dev/null +++ b/tests/@angular_devkit/build_ng_packagr/ng-packaged/tslint.json @@ -0,0 +1,131 @@ +{ + "rulesDirectory": [ + "../../../../node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs", + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/tests/@angular_devkit/build_optimizer/webpack/aio-app/package-lock.json b/tests/@angular_devkit/build_optimizer/webpack/aio-app/package-lock.json index 802013f01c..904c7910d0 100644 --- a/tests/@angular_devkit/build_optimizer/webpack/aio-app/package-lock.json +++ b/tests/@angular_devkit/build_optimizer/webpack/aio-app/package-lock.json @@ -185,17 +185,6 @@ "jshashes": "1.0.7" } }, - "@ngtools/webpack": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.4.1.tgz", - "integrity": "sha1-bUmPIc0a/AJ7i3RthSSn9OkeHas=", - "requires": { - "enhanced-resolve": "3.3.0", - "loader-utils": "1.1.0", - "magic-string": "0.19.1", - "source-map": "0.5.6" - } - }, "@types/jasmine": { "version": "2.5.45", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.5.45.tgz", diff --git a/tests/@angular_devkit/build_optimizer/webpack/aio-app/package.json b/tests/@angular_devkit/build_optimizer/webpack/aio-app/package.json index 36d02f994b..13ce4ef3c1 100644 --- a/tests/@angular_devkit/build_optimizer/webpack/aio-app/package.json +++ b/tests/@angular_devkit/build_optimizer/webpack/aio-app/package.json @@ -9,7 +9,7 @@ "serve": "lite-server", "pree2e": "npm run build -- --devtool=source-map", "e2e": "concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first", - "preprotractor": "webdriver-manager update", + "preprotractor": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.33", "protractor": "protractor protractor.config.js", "benchmark": "npm run build && npm run build-no-ngo && node benchmark.js", "reinstall-bo": "rimraf node_modules/@angular-devkit/build-optimizer && npm i -q ../../../../../dist/@angular-devkit_build-optimizer.tgz" @@ -29,7 +29,7 @@ "@angular/service-worker": "1.0.0-beta.16", "@ngtools/webpack": "1.4.1", "core-js": "^2.4.1", - "rxjs": "^5.1.0", + "rxjs": "^5.5.8", "zone.js": "^0.8.4" }, "devDependencies": { diff --git a/tests/@angular_devkit/build_optimizer/webpack/simple-app/package.json b/tests/@angular_devkit/build_optimizer/webpack/simple-app/package.json index b748e5a8ab..02e2ae0557 100644 --- a/tests/@angular_devkit/build_optimizer/webpack/simple-app/package.json +++ b/tests/@angular_devkit/build_optimizer/webpack/simple-app/package.json @@ -9,7 +9,7 @@ "serve": "lite-server", "pree2e": "npm run build -- --devtool=source-map", "e2e": "concurrently \"npm run serve\" \"npm run protractor\" --kill-others --success first", - "preprotractor": "webdriver-manager update", + "preprotractor": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.33", "protractor": "protractor protractor.config.js", "benchmark": "npm run build && npm run build-no-ngo && node benchmark.js", "reinstall-bo": "rimraf node_modules/@angular-devkit/build-optimizer && npm i -q ../../../../../dist/@angular-devkit_build-optimizer.tgz" @@ -25,7 +25,7 @@ "@angular/router": "^4.2.3", "@ngtools/webpack": "1.4.1", "core-js": "^2.4.1", - "rxjs": "^5.1.0", + "rxjs": "^5.5.8", "zone.js": "^0.8.4" }, "devDependencies": { diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/.angular-cli.json b/tests/@angular_devkit/build_webpack/hello-world-app/.angular-cli.json deleted file mode 100644 index 19f81d409d..0000000000 --- a/tests/@angular_devkit/build_webpack/hello-world-app/.angular-cli.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "project": { - "name": "hello-world-app" - }, - "apps": [ - { - "root": "src", - "outDir": "dist", - "assets": [ - "assets", - "favicon.ico" - ], - "index": "index.html", - "main": "main.ts", - "polyfills": "polyfills.ts", - "test": "test.ts", - "tsconfig": "tsconfig.app.json", - "testTsconfig": "tsconfig.spec.json", - "prefix": "app", - "styles": [ - "styles.css" - ], - "scripts": [], - "environmentSource": "environments/environment.ts", - "environments": { - "dev": "environments/environment.ts", - "prod": "environments/environment.prod.ts" - } - } - ], - "e2e": { - "protractor": { - "config": "./protractor.conf.js" - } - }, - "lint": [ - { - "project": "src/tsconfig.app.json", - "exclude": "**/node_modules/**" - }, - { - "project": "src/tsconfig.spec.json", - "exclude": "**/node_modules/**" - }, - { - "project": "e2e/tsconfig.e2e.json", - "exclude": "**/node_modules/**" - } - ], - "test": { - "karma": { - "config": "./karma.conf.js" - } - }, - "defaults": { - "styleExt": "css", - "component": {} - } -} diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/e2e/tsconfig.e2e.json b/tests/@angular_devkit/build_webpack/hello-world-app/e2e/tsconfig.e2e.json deleted file mode 100644 index 1d9e5edf09..0000000000 --- a/tests/@angular_devkit/build_webpack/hello-world-app/e2e/tsconfig.e2e.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/e2e", - "baseUrl": "./", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -} diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/karma.conf.js b/tests/@angular_devkit/build_webpack/hello-world-app/karma.conf.js index aba937c321..3aaab1c2f0 100644 --- a/tests/@angular_devkit/build_webpack/hello-world-app/karma.conf.js +++ b/tests/@angular_devkit/build_webpack/hello-world-app/karma.conf.js @@ -1,21 +1,24 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html +const path = require('path'); + module.exports = function (config) { config.set({ basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-webpack'], + frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-webpack/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma') ], client:{ clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { + dir: path.join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], fixWebpackSourcePaths: true }, diff --git a/tests/@angular_devkit/build_webpack/hello-world-app/src/typings.d.ts b/tests/@angular_devkit/build_webpack/hello-world-app/src/typings.d.ts deleted file mode 100644 index ef5c7bd620..0000000000 --- a/tests/@angular_devkit/build_webpack/hello-world-app/src/typings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* SystemJS module definition */ -declare var module: NodeModule; -interface NodeModule { - id: string; -} diff --git a/tests/@angular_devkit/core/workspace/angular-workspace.json b/tests/@angular_devkit/core/workspace/angular-workspace.json new file mode 100644 index 0000000000..46c20948ac --- /dev/null +++ b/tests/@angular_devkit/core/workspace/angular-workspace.json @@ -0,0 +1,74 @@ +{ + "version": 1, + "newProjectRoot": "./projects", + "cli": { + "$globalOverride": "${HOME}/.angular-cli.json", + "schematics": { + "defaultCollection": "@schematics/angular" + }, + "warnings": { + "showDeprecation": false + } + }, + "schematics": { + "@schematics/angular": { + "*": { + "skipImport": true, + "packageManager": "yarn" + }, + "application": { + "spec": false + } + } + }, + "architect": {}, + "projects": { + "app": { + "root": "projects/app", + "projectType": "application", + "cli": {}, + "schematics": { + "@schematics/angular": { + "*": { + "spec": false + } + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "transforms": [ + { + "plugin": "@angular-devkit/architect-transforms:replacement", + "file": "environments/environment.ts", + "configurations": { + "production": "environments/environment.prod.ts" + } + } + ], + "options": { + "outputPath": "../dist", + "index": "index.html", + "main": "main.ts", + "polyfills": "polyfills.ts", + "tsConfig": "tsconfig.app.json", + "progress": false + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/schematics/update/packages/update-base/package.json b/tests/schematics/update/packages/update-base/package.json new file mode 100644 index 0000000000..19520c8638 --- /dev/null +++ b/tests/schematics/update/packages/update-base/package.json @@ -0,0 +1,5 @@ +{ + "name": "@angular-devkit-tests/update-base", + "version": "1.1.0", + "description": "Tests" +} diff --git a/tests/schematics/update/packages/update-migrations-external/package.json b/tests/schematics/update/packages/update-migrations-external/package.json new file mode 100644 index 0000000000..adaa40d219 --- /dev/null +++ b/tests/schematics/update/packages/update-migrations-external/package.json @@ -0,0 +1,11 @@ +{ + "name": "@angular-devkit-tests/update-migrations-external", + "version": "1.6.0", + "description": "Tests", + "ng-update": { + "migrations": "@angular-devkit-tests/update-migrations/migrations.json" + }, + "dependencies": { + "@angular-devkit-tests/update-migrations": "1.6.0" + } +} diff --git a/tests/schematics/update/packages/update-migrations/migrations.json b/tests/schematics/update/packages/update-migrations/migrations.json new file mode 100644 index 0000000000..f00f3ffbbd --- /dev/null +++ b/tests/schematics/update/packages/update-migrations/migrations.json @@ -0,0 +1,9 @@ +{ + "schematics": { + "1": { + "factory": "./v1_5.js", + "version": "1.5", + "description": "Test." + } + } +} diff --git a/tests/schematics/update/packages/update-migrations/package.json b/tests/schematics/update/packages/update-migrations/package.json new file mode 100644 index 0000000000..d68cb22a68 --- /dev/null +++ b/tests/schematics/update/packages/update-migrations/package.json @@ -0,0 +1,8 @@ +{ + "name": "@angular-devkit-tests/update-migrations", + "version": "1.6.0", + "description": "Tests", + "ng-update": { + "migrations": "./migrations.json" + } +} diff --git a/tests/schematics/update/packages/update-migrations/v1_5.js b/tests/schematics/update/packages/update-migrations/v1_5.js new file mode 100644 index 0000000000..7e79eda93d --- /dev/null +++ b/tests/schematics/update/packages/update-migrations/v1_5.js @@ -0,0 +1,12 @@ +/** + * @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 + */ +exports.default = function() { + return function(tree) { + tree.create('/version1_5', ''); + }; +}; diff --git a/tests/schematics/update/packages/update-peer-dependencies-1/package.json b/tests/schematics/update/packages/update-peer-dependencies-1/package.json new file mode 100644 index 0000000000..4fac13f171 --- /dev/null +++ b/tests/schematics/update/packages/update-peer-dependencies-1/package.json @@ -0,0 +1,5 @@ +{ + "name": "@angular-devkit-tests/update-peer-dependencies-1", + "version": "1.1.0", + "description": "Tests" +} diff --git a/tests/schematics/update/packages/update-peer-dependencies-2/package.json b/tests/schematics/update/packages/update-peer-dependencies-2/package.json new file mode 100644 index 0000000000..f8624d818e --- /dev/null +++ b/tests/schematics/update/packages/update-peer-dependencies-2/package.json @@ -0,0 +1,8 @@ +{ + "name": "@angular-devkit-tests/update-peer-dependencies-2", + "version": "1.1.0", + "description": "Tests", + "peerDependencies": { + "@angular-devkit-tests/update-peer-dependencies-2": "1.0.0" + } +} diff --git a/tsconfig.json b/tsconfig.json index ff6b600a05..cfcdce05f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,8 @@ "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noImplicitThis": true, - "noUnusedParameters": true, - "noUnusedLocals": true, + "noUnusedParameters": false, // The linter is used for these. + "noUnusedLocals": false, // The linter is used for these. "outDir": "./dist", "rootDir": ".", "skipDefaultLibCheck": true, @@ -38,13 +38,17 @@ "@_/benchmark": [ "./packages/_/benchmark/src/index" ], "@angular-devkit/core": [ "./packages/angular_devkit/core/src/index" ], "@angular-devkit/core/node": [ "./packages/angular_devkit/core/node/index" ], + "@angular-devkit/core/node/testing": [ "./packages/angular_devkit/core/node/testing/index" ], "@angular-devkit/schematics": [ "./packages/angular_devkit/schematics/src/index" ], "@angular-devkit/schematics/tasks": [ "./packages/angular_devkit/schematics/tasks/index" ], "@angular-devkit/schematics/tasks/node": [ "./packages/angular_devkit/schematics/tasks/node/index" ], "@angular-devkit/schematics/tools": [ "./packages/angular_devkit/schematics/tools/index" ], "@angular-devkit/schematics/testing": [ "./packages/angular_devkit/schematics/testing/index" ], "@angular-devkit/build-optimizer": [ "./packages/angular_devkit/build_optimizer/src/index" ], - "@angular-devkit/architect": [ "./packages/angular_devkit/architect/src/index" ] + "@angular-devkit/architect": [ "./packages/angular_devkit/architect/src/index" ], + "@ngtools/webpack": [ "./packages/ngtools/webpack/src/index" ], + "@ngtools/webpack/*": [ "./packages/ngtools/webpack/*" ], + "@schematics/angular": [ "./packages/schematics/angular/index" ] } }, "bazelOptions": { @@ -54,6 +58,7 @@ "bazel-*/**/*", "dist/**/*", "node_modules/**/*", + "packages/_/devkit/**/*files/**/*", "packages/schematics/*/*/*files/**/*", "tmp/**/*", "scripts/patches/**/*", diff --git a/tslint.json b/tslint.json index f0f33ec1de..5ac13c834d 100644 --- a/tslint.json +++ b/tslint.json @@ -11,10 +11,6 @@ "no-implicit-dependencies": true, - "import-blacklist": [ - true, - "rxjs" - ], "no-import-side-effect": [true, {"ignore-module": "^(?!rxjs\/)"}], "align": [ true, @@ -46,6 +42,7 @@ "no-internal-module": true, "no-trailing-whitespace": true, "no-unused-expression": true, + "no-unused-variable": [true, {"ignore-pattern": "^_"}], "no-var-keyword": true, "one-line": [ true,