From fb662c15bb06d6cc438f427df19e966904074b4f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Thu, 10 May 2018 21:29:08 -0500 Subject: [PATCH 1/4] # This is a combination of 4 commits. # This is the 1st commit message: feat: Add ng update support to ngrx packages # The commit message #2 will be skipped: # Copy schematics-core into each package src directory # # Since Bazel builds from source, external consumers will be broken # as they won't run the build step to copy the files # The commit message #3 will be skipped: # Run nyc tests without Bazel build # The commit message #4 will be skipped: # Remove build step from example app --- .circleci/config.yml | 8 +- .gitignore | 2 - .../effects/migrations/6_0_0/index.spec.ts | 39 ++ modules/effects/migrations/6_0_0/index.ts | 6 + modules/effects/migrations/migration.json | 11 + modules/effects/package.json | 3 +- modules/effects/src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ modules/entity/migrations/6_0_0/index.spec.ts | 39 ++ modules/entity/migrations/6_0_0/index.ts | 6 + modules/entity/migrations/migration.json | 11 + modules/entity/package.json | 3 +- modules/entity/src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ .../migrations/6_0_0/index.spec.ts | 39 ++ .../router-store/migrations/6_0_0/index.ts | 6 + .../router-store/migrations/migration.json | 11 + modules/router-store/package.json | 3 +- .../router-store/src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ modules/schematics-core/index.ts | 2 + .../schematics-core/testing/create-package.ts | 26 + modules/schematics-core/testing/update.ts | 2 + modules/schematics-core/utility/update.ts | 47 ++ .../schematics/migrations/6_0_0/index.spec.ts | 39 ++ modules/schematics/migrations/6_0_0/index.ts | 6 + modules/schematics/migrations/migration.json | 11 + modules/schematics/package.json | 5 +- .../schematics/src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ .../migrations/6_0_0/index.spec.ts | 39 ++ .../store-devtools/migrations/6_0_0/index.ts | 6 + .../store-devtools/migrations/migration.json | 11 + modules/store-devtools/package.json | 3 +- .../src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ modules/store/migrations/6_0_0/index.spec.ts | 47 +- modules/store/migrations/6_0_0/index.ts | 44 +- modules/store/src/schematics-core/index.ts | 72 +++ .../testing/create-app-module.ts | 60 ++ .../schematics-core/testing/create-package.ts | 26 + .../testing/create-reducers.ts | 34 ++ .../testing/create-workspace.ts | 55 ++ .../testing/get-file-content.ts | 11 + .../src/schematics-core/testing/index.ts | 4 + .../src/schematics-core/testing/update.ts | 2 + .../src/schematics-core/utility/ast-utils.ts | 539 ++++++++++++++++++ .../schematics-core/utility/ast-utils_spec.ts | 87 +++ .../src/schematics-core/utility/change.ts | 135 +++++ .../src/schematics-core/utility/config.ts | 487 ++++++++++++++++ .../schematics-core/utility/find-module.ts | 120 ++++ .../utility/find-module_spec.ts | 63 ++ .../src/schematics-core/utility/ngrx-utils.ts | 257 +++++++++ .../src/schematics-core/utility/project.ts | 28 + .../schematics-core/utility/route-utils.ts | 110 ++++ .../src/schematics-core/utility/strings.ts | 152 +++++ .../src/schematics-core/utility/update.ts | 47 ++ package.json | 6 +- 143 files changed, 14134 insertions(+), 81 deletions(-) create mode 100644 modules/effects/migrations/6_0_0/index.spec.ts create mode 100644 modules/effects/migrations/6_0_0/index.ts create mode 100644 modules/effects/migrations/migration.json create mode 100644 modules/effects/src/schematics-core/index.ts create mode 100644 modules/effects/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/effects/src/schematics-core/testing/create-package.ts create mode 100644 modules/effects/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/effects/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/effects/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/effects/src/schematics-core/testing/index.ts create mode 100644 modules/effects/src/schematics-core/testing/update.ts create mode 100644 modules/effects/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/effects/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/effects/src/schematics-core/utility/change.ts create mode 100644 modules/effects/src/schematics-core/utility/config.ts create mode 100644 modules/effects/src/schematics-core/utility/find-module.ts create mode 100644 modules/effects/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/effects/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/effects/src/schematics-core/utility/project.ts create mode 100644 modules/effects/src/schematics-core/utility/route-utils.ts create mode 100644 modules/effects/src/schematics-core/utility/strings.ts create mode 100644 modules/effects/src/schematics-core/utility/update.ts create mode 100644 modules/entity/migrations/6_0_0/index.spec.ts create mode 100644 modules/entity/migrations/6_0_0/index.ts create mode 100644 modules/entity/migrations/migration.json create mode 100644 modules/entity/src/schematics-core/index.ts create mode 100644 modules/entity/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/entity/src/schematics-core/testing/create-package.ts create mode 100644 modules/entity/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/entity/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/entity/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/entity/src/schematics-core/testing/index.ts create mode 100644 modules/entity/src/schematics-core/testing/update.ts create mode 100644 modules/entity/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/entity/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/entity/src/schematics-core/utility/change.ts create mode 100644 modules/entity/src/schematics-core/utility/config.ts create mode 100644 modules/entity/src/schematics-core/utility/find-module.ts create mode 100644 modules/entity/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/entity/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/entity/src/schematics-core/utility/project.ts create mode 100644 modules/entity/src/schematics-core/utility/route-utils.ts create mode 100644 modules/entity/src/schematics-core/utility/strings.ts create mode 100644 modules/entity/src/schematics-core/utility/update.ts create mode 100644 modules/router-store/migrations/6_0_0/index.spec.ts create mode 100644 modules/router-store/migrations/6_0_0/index.ts create mode 100644 modules/router-store/migrations/migration.json create mode 100644 modules/router-store/src/schematics-core/index.ts create mode 100644 modules/router-store/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/router-store/src/schematics-core/testing/create-package.ts create mode 100644 modules/router-store/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/router-store/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/router-store/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/router-store/src/schematics-core/testing/index.ts create mode 100644 modules/router-store/src/schematics-core/testing/update.ts create mode 100644 modules/router-store/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/router-store/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/router-store/src/schematics-core/utility/change.ts create mode 100644 modules/router-store/src/schematics-core/utility/config.ts create mode 100644 modules/router-store/src/schematics-core/utility/find-module.ts create mode 100644 modules/router-store/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/router-store/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/router-store/src/schematics-core/utility/project.ts create mode 100644 modules/router-store/src/schematics-core/utility/route-utils.ts create mode 100644 modules/router-store/src/schematics-core/utility/strings.ts create mode 100644 modules/router-store/src/schematics-core/utility/update.ts create mode 100644 modules/schematics-core/testing/create-package.ts create mode 100644 modules/schematics-core/testing/update.ts create mode 100644 modules/schematics-core/utility/update.ts create mode 100644 modules/schematics/migrations/6_0_0/index.spec.ts create mode 100644 modules/schematics/migrations/6_0_0/index.ts create mode 100644 modules/schematics/migrations/migration.json create mode 100644 modules/schematics/src/schematics-core/index.ts create mode 100644 modules/schematics/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/schematics/src/schematics-core/testing/create-package.ts create mode 100644 modules/schematics/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/schematics/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/schematics/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/schematics/src/schematics-core/testing/index.ts create mode 100644 modules/schematics/src/schematics-core/testing/update.ts create mode 100644 modules/schematics/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/schematics/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/schematics/src/schematics-core/utility/change.ts create mode 100644 modules/schematics/src/schematics-core/utility/config.ts create mode 100644 modules/schematics/src/schematics-core/utility/find-module.ts create mode 100644 modules/schematics/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/schematics/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/schematics/src/schematics-core/utility/project.ts create mode 100644 modules/schematics/src/schematics-core/utility/route-utils.ts create mode 100644 modules/schematics/src/schematics-core/utility/strings.ts create mode 100644 modules/schematics/src/schematics-core/utility/update.ts create mode 100644 modules/store-devtools/migrations/6_0_0/index.spec.ts create mode 100644 modules/store-devtools/migrations/6_0_0/index.ts create mode 100644 modules/store-devtools/migrations/migration.json create mode 100644 modules/store-devtools/src/schematics-core/index.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/create-package.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/index.ts create mode 100644 modules/store-devtools/src/schematics-core/testing/update.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/change.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/config.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/find-module.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/project.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/route-utils.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/strings.ts create mode 100644 modules/store-devtools/src/schematics-core/utility/update.ts create mode 100644 modules/store/src/schematics-core/index.ts create mode 100644 modules/store/src/schematics-core/testing/create-app-module.ts create mode 100644 modules/store/src/schematics-core/testing/create-package.ts create mode 100644 modules/store/src/schematics-core/testing/create-reducers.ts create mode 100644 modules/store/src/schematics-core/testing/create-workspace.ts create mode 100644 modules/store/src/schematics-core/testing/get-file-content.ts create mode 100644 modules/store/src/schematics-core/testing/index.ts create mode 100644 modules/store/src/schematics-core/testing/update.ts create mode 100644 modules/store/src/schematics-core/utility/ast-utils.ts create mode 100644 modules/store/src/schematics-core/utility/ast-utils_spec.ts create mode 100644 modules/store/src/schematics-core/utility/change.ts create mode 100644 modules/store/src/schematics-core/utility/config.ts create mode 100644 modules/store/src/schematics-core/utility/find-module.ts create mode 100644 modules/store/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/store/src/schematics-core/utility/ngrx-utils.ts create mode 100644 modules/store/src/schematics-core/utility/project.ts create mode 100644 modules/store/src/schematics-core/utility/route-utils.ts create mode 100644 modules/store/src/schematics-core/utility/strings.ts create mode 100644 modules/store/src/schematics-core/utility/update.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index d08af66ef3..4f78848aeb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,9 +54,6 @@ jobs: # Install the dependencies from NPM, using the node and yarn binaries managed by Bazel - run: bazel run @yarn//:yarn - # Copy shared schematics-core into schematics/src directory - - run: yarn copy:schematics - # Build and Test # Use bazel query so that we explicitly ask for all buildable targets to # be built even though we run `bazel test` @@ -64,13 +61,13 @@ jobs: - run: bazel query --output=label //... | xargs bazel test test: - <<: *run_in_ngcontainer + docker: + - image: circleci/node:latest-browsers steps: - checkout - restore_cache: key: *cache_key - run: yarn - - run: yarn copy:schematics - run: yarn run ci - run: yarn run example:build:prod - run: yarn run example:test --watch=false @@ -89,7 +86,6 @@ jobs: - restore_cache: key: *cache_key - run: yarn - - run: yarn copy:schematics - run: yarn run build - run: yarn run deploy:builds diff --git a/.gitignore b/.gitignore index 40a4674c46..4c81f88397 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,4 @@ tmp example-dist/ -schematics-core -!modules/schematics-core *.tgz \ No newline at end of file diff --git a/modules/effects/migrations/6_0_0/index.spec.ts b/modules/effects/migrations/6_0_0/index.spec.ts new file mode 100644 index 0000000000..9499ac1cf6 --- /dev/null +++ b/modules/effects/migrations/6_0_0/index.spec.ts @@ -0,0 +1,39 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; + +const collectionPath = path.join(__dirname, '../migration.json'); + +describe('Effects Migration 6_0_0', () => { + let appTree; + const pkgName = 'effects'; + + versionPrefixes.forEach(prefix => { + it(`should install version ${prefix}6.0.0`, () => { + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); + const pkg = JSON.parse(newTree.readContent(packagePath)); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); + }); + }); +}); diff --git a/modules/effects/migrations/6_0_0/index.ts b/modules/effects/migrations/6_0_0/index.ts new file mode 100644 index 0000000000..e92e75a56e --- /dev/null +++ b/modules/effects/migrations/6_0_0/index.ts @@ -0,0 +1,6 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; + +export default function(): Rule { + return updatePackage('effects'); +} diff --git a/modules/effects/migrations/migration.json b/modules/effects/migrations/migration.json new file mode 100644 index 0000000000..9e8503b6e1 --- /dev/null +++ b/modules/effects/migrations/migration.json @@ -0,0 +1,11 @@ +{ + "$schema": + "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ngrx-effects-migration-01": { + "description": "The road to v6", + "version": "5.2", + "factory": "./6_0_0/index" + } + } +} diff --git a/modules/effects/package.json b/modules/effects/package.json index 586a593275..1d57c2fe70 100644 --- a/modules/effects/package.json +++ b/modules/effects/package.json @@ -18,7 +18,8 @@ "rxjs": "RXJS_VERSION" }, "ng-update": { - "packageGroup": "NG_UPDATE_PACKAGE_GROUP" + "packageGroup": "NG_UPDATE_PACKAGE_GROUP", + "migrations": "NG_UPDATE_MIGRATIONS" }, "sideEffects": false } diff --git a/modules/effects/src/schematics-core/index.ts b/modules/effects/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/effects/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/effects/src/schematics-core/testing/create-app-module.ts b/modules/effects/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/effects/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/effects/src/schematics-core/testing/create-package.ts b/modules/effects/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/effects/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/effects/src/schematics-core/testing/create-reducers.ts b/modules/effects/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/effects/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/effects/src/schematics-core/testing/create-workspace.ts b/modules/effects/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/effects/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/effects/src/schematics-core/testing/get-file-content.ts b/modules/effects/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/effects/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/effects/src/schematics-core/testing/index.ts b/modules/effects/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/effects/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/effects/src/schematics-core/testing/update.ts b/modules/effects/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/effects/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/effects/src/schematics-core/utility/ast-utils.ts b/modules/effects/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/effects/src/schematics-core/utility/ast-utils_spec.ts b/modules/effects/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/effects/src/schematics-core/utility/change.ts b/modules/effects/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/effects/src/schematics-core/utility/config.ts b/modules/effects/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/effects/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/effects/src/schematics-core/utility/find-module.ts b/modules/effects/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/effects/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/effects/src/schematics-core/utility/find-module_spec.ts b/modules/effects/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/effects/src/schematics-core/utility/ngrx-utils.ts b/modules/effects/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/effects/src/schematics-core/utility/project.ts b/modules/effects/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/effects/src/schematics-core/utility/route-utils.ts b/modules/effects/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/effects/src/schematics-core/utility/strings.ts b/modules/effects/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/effects/src/schematics-core/utility/update.ts b/modules/effects/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/effects/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/entity/migrations/6_0_0/index.spec.ts b/modules/entity/migrations/6_0_0/index.spec.ts new file mode 100644 index 0000000000..12041b1cbc --- /dev/null +++ b/modules/entity/migrations/6_0_0/index.spec.ts @@ -0,0 +1,39 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; + +const collectionPath = path.join(__dirname, '../migration.json'); + +describe('Entity Migration 6_0_0', () => { + let appTree; + const pkgName = 'entity'; + + versionPrefixes.forEach(prefix => { + it(`should install version ${prefix}6.0.0`, () => { + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); + const pkg = JSON.parse(newTree.readContent(packagePath)); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); + }); + }); +}); diff --git a/modules/entity/migrations/6_0_0/index.ts b/modules/entity/migrations/6_0_0/index.ts new file mode 100644 index 0000000000..b344fe4a5a --- /dev/null +++ b/modules/entity/migrations/6_0_0/index.ts @@ -0,0 +1,6 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; + +export default function(): Rule { + return updatePackage('entity'); +} diff --git a/modules/entity/migrations/migration.json b/modules/entity/migrations/migration.json new file mode 100644 index 0000000000..78e5c75f4a --- /dev/null +++ b/modules/entity/migrations/migration.json @@ -0,0 +1,11 @@ +{ + "$schema": + "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ngrx-entity-migration-01": { + "description": "The road to v6", + "version": "5.2", + "factory": "./6_0_0/index" + } + } +} diff --git a/modules/entity/package.json b/modules/entity/package.json index 09915a58ea..9234896bd6 100644 --- a/modules/entity/package.json +++ b/modules/entity/package.json @@ -18,7 +18,8 @@ "rxjs": "RXJS_VERSION" }, "ng-update": { - "packageGroup": "NG_UPDATE_PACKAGE_GROUP" + "packageGroup": "NG_UPDATE_PACKAGE_GROUP", + "migrations": "NG_UPDATE_MIGRATIONS" }, "sideEffects": false } diff --git a/modules/entity/src/schematics-core/index.ts b/modules/entity/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/entity/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/entity/src/schematics-core/testing/create-app-module.ts b/modules/entity/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/entity/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/entity/src/schematics-core/testing/create-package.ts b/modules/entity/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/entity/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/entity/src/schematics-core/testing/create-reducers.ts b/modules/entity/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/entity/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/entity/src/schematics-core/testing/create-workspace.ts b/modules/entity/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/entity/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/entity/src/schematics-core/testing/get-file-content.ts b/modules/entity/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/entity/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/entity/src/schematics-core/testing/index.ts b/modules/entity/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/entity/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/entity/src/schematics-core/testing/update.ts b/modules/entity/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/entity/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/entity/src/schematics-core/utility/ast-utils.ts b/modules/entity/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/entity/src/schematics-core/utility/ast-utils_spec.ts b/modules/entity/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/entity/src/schematics-core/utility/change.ts b/modules/entity/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/entity/src/schematics-core/utility/config.ts b/modules/entity/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/entity/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/entity/src/schematics-core/utility/find-module.ts b/modules/entity/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/entity/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/entity/src/schematics-core/utility/find-module_spec.ts b/modules/entity/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/entity/src/schematics-core/utility/ngrx-utils.ts b/modules/entity/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/entity/src/schematics-core/utility/project.ts b/modules/entity/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/entity/src/schematics-core/utility/route-utils.ts b/modules/entity/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/entity/src/schematics-core/utility/strings.ts b/modules/entity/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/entity/src/schematics-core/utility/update.ts b/modules/entity/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/entity/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/router-store/migrations/6_0_0/index.spec.ts b/modules/router-store/migrations/6_0_0/index.spec.ts new file mode 100644 index 0000000000..5d65de72f3 --- /dev/null +++ b/modules/router-store/migrations/6_0_0/index.spec.ts @@ -0,0 +1,39 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; + +const collectionPath = path.join(__dirname, '../migration.json'); + +describe('Router Store Migration 6_0_0', () => { + let appTree; + const pkgName = 'router-store'; + + versionPrefixes.forEach(prefix => { + it(`should install version ${prefix}6.0.0`, () => { + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); + const pkg = JSON.parse(newTree.readContent(packagePath)); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); + }); + }); +}); diff --git a/modules/router-store/migrations/6_0_0/index.ts b/modules/router-store/migrations/6_0_0/index.ts new file mode 100644 index 0000000000..e7790ce765 --- /dev/null +++ b/modules/router-store/migrations/6_0_0/index.ts @@ -0,0 +1,6 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; + +export default function(): Rule { + return updatePackage('router-store'); +} diff --git a/modules/router-store/migrations/migration.json b/modules/router-store/migrations/migration.json new file mode 100644 index 0000000000..c77cd6c1ec --- /dev/null +++ b/modules/router-store/migrations/migration.json @@ -0,0 +1,11 @@ +{ + "$schema": + "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ngrx-router-store-migration-01": { + "description": "The road to v6", + "version": "5.2", + "factory": "./6_0_0/index" + } + } +} diff --git a/modules/router-store/package.json b/modules/router-store/package.json index c8b96a35fc..3e318865df 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -25,7 +25,8 @@ "rxjs": "RXJS_VERSION" }, "ng-update": { - "packageGroup": "NG_UPDATE_PACKAGE_GROUP" + "packageGroup": "NG_UPDATE_PACKAGE_GROUP", + "migrations": "NG_UPDATE_MIGRATIONS" }, "sideEffects": false } diff --git a/modules/router-store/src/schematics-core/index.ts b/modules/router-store/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/router-store/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/router-store/src/schematics-core/testing/create-app-module.ts b/modules/router-store/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/router-store/src/schematics-core/testing/create-package.ts b/modules/router-store/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/router-store/src/schematics-core/testing/create-reducers.ts b/modules/router-store/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/router-store/src/schematics-core/testing/create-workspace.ts b/modules/router-store/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/router-store/src/schematics-core/testing/get-file-content.ts b/modules/router-store/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/router-store/src/schematics-core/testing/index.ts b/modules/router-store/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/router-store/src/schematics-core/testing/update.ts b/modules/router-store/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/router-store/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/router-store/src/schematics-core/utility/ast-utils.ts b/modules/router-store/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts b/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/router-store/src/schematics-core/utility/change.ts b/modules/router-store/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/router-store/src/schematics-core/utility/config.ts b/modules/router-store/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/router-store/src/schematics-core/utility/find-module.ts b/modules/router-store/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/router-store/src/schematics-core/utility/find-module_spec.ts b/modules/router-store/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/router-store/src/schematics-core/utility/ngrx-utils.ts b/modules/router-store/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/router-store/src/schematics-core/utility/project.ts b/modules/router-store/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/router-store/src/schematics-core/utility/route-utils.ts b/modules/router-store/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/router-store/src/schematics-core/utility/strings.ts b/modules/router-store/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/router-store/src/schematics-core/utility/update.ts b/modules/router-store/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/router-store/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/schematics-core/index.ts b/modules/schematics-core/index.ts index 8c37240cfb..9c715f8dd4 100644 --- a/modules/schematics-core/index.ts +++ b/modules/schematics-core/index.ts @@ -68,3 +68,5 @@ export const stringUtils = { capitalize, featurePath, }; + +export { updatePackage } from './utility/update'; diff --git a/modules/schematics-core/testing/create-package.ts b/modules/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/schematics-core/testing/update.ts b/modules/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/schematics-core/utility/update.ts b/modules/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/schematics/migrations/6_0_0/index.spec.ts b/modules/schematics/migrations/6_0_0/index.spec.ts new file mode 100644 index 0000000000..ff0dcfac3f --- /dev/null +++ b/modules/schematics/migrations/6_0_0/index.spec.ts @@ -0,0 +1,39 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; + +const collectionPath = path.join(__dirname, '../migration.json'); + +describe('Schematics Migration 6_0_0', () => { + let appTree; + const pkgName = 'schematics'; + + versionPrefixes.forEach(prefix => { + it(`should install version ${prefix}6.0.0`, () => { + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); + const pkg = JSON.parse(newTree.readContent(packagePath)); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); + }); + }); +}); diff --git a/modules/schematics/migrations/6_0_0/index.ts b/modules/schematics/migrations/6_0_0/index.ts new file mode 100644 index 0000000000..f2329b24d6 --- /dev/null +++ b/modules/schematics/migrations/6_0_0/index.ts @@ -0,0 +1,6 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; + +export default function(): Rule { + return updatePackage('schematics'); +} diff --git a/modules/schematics/migrations/migration.json b/modules/schematics/migrations/migration.json new file mode 100644 index 0000000000..47e17a3c08 --- /dev/null +++ b/modules/schematics/migrations/migration.json @@ -0,0 +1,11 @@ +{ + "$schema": + "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ngrx-schematics-migration-01": { + "description": "The road to v6", + "version": "5.2", + "factory": "./6_0_0/index" + } + } +} diff --git a/modules/schematics/package.json b/modules/schematics/package.json index edbeaaf60c..d1b9e0068c 100644 --- a/modules/schematics/package.json +++ b/modules/schematics/package.json @@ -22,8 +22,9 @@ "homepage": "https://github.com/ngrx/platform#readme", "schematics": "./collection.json", "ng-update": { - "packageGroup": "NG_UPDATE_PACKAGE_GROUP" - }, + "packageGroup": "NG_UPDATE_PACKAGE_GROUP", + "migrations": "NG_UPDATE_MIGRATIONS" + }, "peerDependencies": { "@angular-devkit/core": "NG_DEVKIT_VERSION", "@angular-devkit/schematics": "NG_DEVKIT_VERSION" diff --git a/modules/schematics/src/schematics-core/index.ts b/modules/schematics/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/schematics/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/schematics/src/schematics-core/testing/create-app-module.ts b/modules/schematics/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/schematics/src/schematics-core/testing/create-package.ts b/modules/schematics/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/schematics/src/schematics-core/testing/create-reducers.ts b/modules/schematics/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/schematics/src/schematics-core/testing/create-workspace.ts b/modules/schematics/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/schematics/src/schematics-core/testing/get-file-content.ts b/modules/schematics/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/schematics/src/schematics-core/testing/index.ts b/modules/schematics/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/schematics/src/schematics-core/testing/update.ts b/modules/schematics/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/schematics/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/schematics/src/schematics-core/utility/ast-utils.ts b/modules/schematics/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts b/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/schematics/src/schematics-core/utility/change.ts b/modules/schematics/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/schematics/src/schematics-core/utility/config.ts b/modules/schematics/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/schematics/src/schematics-core/utility/find-module.ts b/modules/schematics/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/schematics/src/schematics-core/utility/find-module_spec.ts b/modules/schematics/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/schematics/src/schematics-core/utility/ngrx-utils.ts b/modules/schematics/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/schematics/src/schematics-core/utility/project.ts b/modules/schematics/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/schematics/src/schematics-core/utility/route-utils.ts b/modules/schematics/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/schematics/src/schematics-core/utility/strings.ts b/modules/schematics/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/schematics/src/schematics-core/utility/update.ts b/modules/schematics/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/schematics/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/store-devtools/migrations/6_0_0/index.spec.ts b/modules/store-devtools/migrations/6_0_0/index.spec.ts new file mode 100644 index 0000000000..55dc9d4436 --- /dev/null +++ b/modules/store-devtools/migrations/6_0_0/index.spec.ts @@ -0,0 +1,39 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; + +const collectionPath = path.join(__dirname, '../migration.json'); + +describe('Store Devtools Migration 6_0_0', () => { + let appTree; + const pkgName = 'store-devtools'; + + versionPrefixes.forEach(prefix => { + it(`should install version ${prefix}6.0.0`, () => { + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); + const pkg = JSON.parse(newTree.readContent(packagePath)); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); + }); + }); +}); diff --git a/modules/store-devtools/migrations/6_0_0/index.ts b/modules/store-devtools/migrations/6_0_0/index.ts new file mode 100644 index 0000000000..da9e5e6c76 --- /dev/null +++ b/modules/store-devtools/migrations/6_0_0/index.ts @@ -0,0 +1,6 @@ +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; + +export default function(): Rule { + return updatePackage('store-devtools'); +} diff --git a/modules/store-devtools/migrations/migration.json b/modules/store-devtools/migrations/migration.json new file mode 100644 index 0000000000..48f8345553 --- /dev/null +++ b/modules/store-devtools/migrations/migration.json @@ -0,0 +1,11 @@ +{ + "$schema": + "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ngrx-store-devtools-migration-01": { + "description": "The road to v6", + "version": "5.2", + "factory": "./6_0_0/index" + } + } +} diff --git a/modules/store-devtools/package.json b/modules/store-devtools/package.json index c4951bec75..b8aa7af36c 100644 --- a/modules/store-devtools/package.json +++ b/modules/store-devtools/package.json @@ -24,7 +24,8 @@ "rxjs": "RXJS_VERSION" }, "ng-update": { - "packageGroup": "NG_UPDATE_PACKAGE_GROUP" + "packageGroup": "NG_UPDATE_PACKAGE_GROUP", + "migrations": "NG_UPDATE_MIGRATIONS" }, "sideEffects": false } diff --git a/modules/store-devtools/src/schematics-core/index.ts b/modules/store-devtools/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/store-devtools/src/schematics-core/testing/create-app-module.ts b/modules/store-devtools/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/store-devtools/src/schematics-core/testing/create-package.ts b/modules/store-devtools/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/store-devtools/src/schematics-core/testing/create-reducers.ts b/modules/store-devtools/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/store-devtools/src/schematics-core/testing/create-workspace.ts b/modules/store-devtools/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/store-devtools/src/schematics-core/testing/get-file-content.ts b/modules/store-devtools/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/store-devtools/src/schematics-core/testing/index.ts b/modules/store-devtools/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/store-devtools/src/schematics-core/testing/update.ts b/modules/store-devtools/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/store-devtools/src/schematics-core/utility/ast-utils.ts b/modules/store-devtools/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts b/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/store-devtools/src/schematics-core/utility/change.ts b/modules/store-devtools/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/store-devtools/src/schematics-core/utility/config.ts b/modules/store-devtools/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/store-devtools/src/schematics-core/utility/find-module.ts b/modules/store-devtools/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts b/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts b/modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/store-devtools/src/schematics-core/utility/project.ts b/modules/store-devtools/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/store-devtools/src/schematics-core/utility/route-utils.ts b/modules/store-devtools/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/store-devtools/src/schematics-core/utility/strings.ts b/modules/store-devtools/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/store-devtools/src/schematics-core/utility/update.ts b/modules/store-devtools/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/store-devtools/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/modules/store/migrations/6_0_0/index.spec.ts b/modules/store/migrations/6_0_0/index.spec.ts index cd45c05835..3a7f2020ec 100644 --- a/modules/store/migrations/6_0_0/index.spec.ts +++ b/modules/store/migrations/6_0_0/index.spec.ts @@ -4,35 +4,36 @@ import { UnitTestTree, } from '@angular-devkit/schematics/testing'; import * as path from 'path'; +import { + createPackageJson, + packagePath, +} from '../../../schematics-core/testing/create-package'; +import { + upgradeVersion, + versionPrefixes, +} from '../../../schematics-core/testing/update'; -const packagePath = '/package.json'; const collectionPath = path.join(__dirname, '../migration.json'); -describe('Migration 6_0_0', () => { - function setup(prefix: string) { - const tree = Tree.empty() as UnitTestTree; - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/store": "${prefix}5.2.0" - } - }` - ); +describe('Store Migration 6_0_0', () => { + let appTree; + const pkgName = 'store'; - return { - tree, - runner: new SchematicTestRunner('schematics', collectionPath), - }; - } - - const prefixes = ['~', '^', '']; - prefixes.forEach(prefix => { + versionPrefixes.forEach(prefix => { it(`should install version ${prefix}6.0.0`, () => { - const { runner, tree } = setup(prefix); - const newTree = runner.runSchematic('ngrx-store-migration-01', {}, tree); + appTree = new UnitTestTree(Tree.empty()); + const runner = new SchematicTestRunner('schematics', collectionPath); + const tree = createPackageJson(prefix, pkgName, appTree); + + const newTree = runner.runSchematic( + `ngrx-${pkgName}-migration-01`, + {}, + tree + ); const pkg = JSON.parse(newTree.readContent(packagePath)); - expect(pkg.dependencies['@ngrx/store']).toBe(`${prefix}6.0.0-beta.2`); + expect(pkg.dependencies[`@ngrx/${pkgName}`]).toBe( + `${prefix}${upgradeVersion}` + ); }); }); }); diff --git a/modules/store/migrations/6_0_0/index.ts b/modules/store/migrations/6_0_0/index.ts index 0172780b20..ba2321f7a3 100644 --- a/modules/store/migrations/6_0_0/index.ts +++ b/modules/store/migrations/6_0_0/index.ts @@ -1,44 +1,6 @@ -import { - Rule, - SchematicContext, - Tree, - SchematicsException, - chain, -} from '@angular-devkit/schematics'; -import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { Rule } from '@angular-devkit/schematics'; +import { updatePackage } from '../../src/schematics-core'; export default function(): Rule { - return (tree: Tree, context: SchematicContext) => { - const pkgPath = '/package.json'; - const buffer = tree.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.dependencies) { - pkg.dependencies = {}; - } - - if (pkg.dependencies['@ngrx/store']) { - const firstChar = pkg.dependencies['@ngrx/store'][0]; - const suffix = match(firstChar, '^') || match(firstChar, '~'); - - // TODO: remove beta - pkg.dependencies['@ngrx/store'] = `${suffix}6.0.0-beta.2`; - tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); - context.addTask(new NodePackageInstallTask()); - } - - return tree; - }; -} - -function match(value: string, test: string) { - return value === test ? test : ''; + return updatePackage('store'); } diff --git a/modules/store/src/schematics-core/index.ts b/modules/store/src/schematics-core/index.ts new file mode 100644 index 0000000000..9c715f8dd4 --- /dev/null +++ b/modules/store/src/schematics-core/index.ts @@ -0,0 +1,72 @@ +import { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +} from './utility/strings'; + +export { + findNodes, + getSourceNodes, + getDecoratorMetadata, + getContentOfKeyLiteral, + insertAfterLastOccurrence, + addBootstrapToModule, + addDeclarationToModule, + addExportToModule, + addImportToModule, + addProviderToModule, +} from './utility/ast-utils'; + +export { + Host, + Change, + NoopChange, + InsertChange, + RemoveChange, + ReplaceChange, +} from './utility/change'; + +export { + AppConfig, + CliConfig, + getAppFromConfig, + getConfig, + getWorkspace, + getWorkspacePath, +} from './utility/config'; + +export { + findModule, + findModuleFromOptions, + buildRelativePath, + ModuleOptions, +} from './utility/find-module'; + +export { + addReducerToState, + addReducerToStateInferface, + addReducerImportToNgModule, + addReducerToActionReducerMap, + omit, +} from './utility/ngrx-utils'; + +export { getProjectPath } from './utility/project'; +export { insertImport } from './utility/route-utils'; + +export const stringUtils = { + dasherize, + decamelize, + camelize, + classify, + underscore, + group, + capitalize, + featurePath, +}; + +export { updatePackage } from './utility/update'; diff --git a/modules/store/src/schematics-core/testing/create-app-module.ts b/modules/store/src/schematics-core/testing/create-app-module.ts new file mode 100644 index 0000000000..ebf3b8274c --- /dev/null +++ b/modules/store/src/schematics-core/testing/create-app-module.ts @@ -0,0 +1,60 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +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'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} + +export function createAppModuleWithEffects( + tree: UnitTestTree, + path: string, + effects?: string +): UnitTestTree { + tree.create( + path || '/src/app/app.module.ts', + ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + import { EffectsModule } from '@ngrx/effects'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ${effects} + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + ` + ); + + return tree; +} diff --git a/modules/store/src/schematics-core/testing/create-package.ts b/modules/store/src/schematics-core/testing/create-package.ts new file mode 100644 index 0000000000..441c85180d --- /dev/null +++ b/modules/store/src/schematics-core/testing/create-package.ts @@ -0,0 +1,26 @@ +import { Tree } from '@angular-devkit/schematics'; +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +export const packagePath = '/package.json'; + +export function createPackageJson( + prefix: string, + pkg: string, + tree: UnitTestTree, + version = '5.2.0', + packagePath = '/package.json' +) { + tree.create( + packagePath, + `{ + "dependencies": { + "@ngrx/${pkg}": "${prefix}5.2.0" + } + }` + ); + + return tree; +} diff --git a/modules/store/src/schematics-core/testing/create-reducers.ts b/modules/store/src/schematics-core/testing/create-reducers.ts new file mode 100644 index 0000000000..9ee0bd4eb7 --- /dev/null +++ b/modules/store/src/schematics-core/testing/create-reducers.ts @@ -0,0 +1,34 @@ +import { UnitTestTree } from '@angular-devkit/schematics/testing'; + +export function createReducers( + tree: UnitTestTree, + path?: string, + project = 'bar' +) { + tree.create( + path || `/projects/${project}/src/app/reducers/index.ts`, + ` + import { + ActionReducer, + ActionReducerMap, + createFeatureSelector, + createSelector, + MetaReducer + } from '@ngrx/store'; + import { environment } from '../../environments/environment'; + + export interface State { + + } + + export const reducers: ActionReducerMap = { + + }; + + + export const metaReducers: MetaReducer[] = !environment.production ? [] : []; + ` + ); + + return tree; +} diff --git a/modules/store/src/schematics-core/testing/create-workspace.ts b/modules/store/src/schematics-core/testing/create-workspace.ts new file mode 100644 index 0000000000..cc6e357255 --- /dev/null +++ b/modules/store/src/schematics-core/testing/create-workspace.ts @@ -0,0 +1,55 @@ +import { + UnitTestTree, + SchematicTestRunner, +} from '@angular-devkit/schematics/testing'; + +const defaultWorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '6.0.0', +}; + +const defaultAppOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'Emulated', + routing: false, + style: 'css', + skipTests: false, +}; + +const defaultModuleOptions = { + name: 'foo', + spec: true, + module: undefined, + flat: false, +}; + +export function getTestProjectPath( + workspaceOptions: any = defaultWorkspaceOptions, + appOptions: any = defaultAppOptions +) { + return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; +} + +export function createWorkspace( + schematicRunner: SchematicTestRunner, + appTree: UnitTestTree, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'workspace', + workspaceOptions + ); + appTree = schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + appOptions, + appTree + ); + + return appTree; +} diff --git a/modules/store/src/schematics-core/testing/get-file-content.ts b/modules/store/src/schematics-core/testing/get-file-content.ts new file mode 100644 index 0000000000..44915d283e --- /dev/null +++ b/modules/store/src/schematics-core/testing/get-file-content.ts @@ -0,0 +1,11 @@ +import { Tree } from '@angular-devkit/schematics'; + +export function getFileContent(tree: Tree, path: string): string { + const fileEntry = tree.get(path); + + if (!fileEntry) { + throw new Error(`The file (${path}) does not exist.`); + } + + return fileEntry.content.toString(); +} diff --git a/modules/store/src/schematics-core/testing/index.ts b/modules/store/src/schematics-core/testing/index.ts new file mode 100644 index 0000000000..894bca7d98 --- /dev/null +++ b/modules/store/src/schematics-core/testing/index.ts @@ -0,0 +1,4 @@ +export * from './create-app-module'; +export * from './create-reducers'; +export * from './create-workspace'; +export * from './get-file-content'; diff --git a/modules/store/src/schematics-core/testing/update.ts b/modules/store/src/schematics-core/testing/update.ts new file mode 100644 index 0000000000..d451bf0513 --- /dev/null +++ b/modules/store/src/schematics-core/testing/update.ts @@ -0,0 +1,2 @@ +export const upgradeVersion = '6.0.0-beta.2'; +export const versionPrefixes = ['~', '^', '']; diff --git a/modules/store/src/schematics-core/utility/ast-utils.ts b/modules/store/src/schematics-core/utility/ast-utils.ts new file mode 100644 index 0000000000..1bd4484651 --- /dev/null +++ b/modules/store/src/schematics-core/utility/ast-utils.ts @@ -0,0 +1,539 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes( + node: ts.Node, + kind: ts.SyntaxKind, + max = Infinity +): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence( + nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind +): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind) + .sort(nodesByPosition) + .pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error( + `tried to insert ${toInsert} as first occurence with no fallback position` + ); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + +export 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; + } +} + +function _angularImportsFromNode( + node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile +): { [name: string]: string } { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map( + (is: ts.ImportSpecifier) => + is.propertyName ? is.propertyName.text : is.name.text + ) + .reduce((acc: { [name: string]: string }, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + +export function getDecoratorMetadata( + source: ts.SourceFile, + identifier: string, + module: string +): ts.Node[] { + const angularImports: { [name: string]: string } = findNodes( + source, + ts.SyntaxKind.ImportDeclaration + ) + .map(node => _angularImportsFromNode(node as ts.ImportDeclaration, source)) + .reduce( + ( + acc: { [name: string]: string }, + current: { [name: string]: string } + ) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, + {} + ); + + return getSourceNodes(source) + .filter(node => { + return ( + node.kind == ts.SyntaxKind.Decorator && + (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression + ); + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return ( + id.getFullText(source) == identifier && + angularImports[id.getFullText(source)] === module + ); + } else if ( + expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression + ) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && angularImports[moduleId + '.'] === module; + } + + return false; + }) + .filter( + expr => + expr.arguments[0] && + expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression + ) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +function _addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = nodes[0]; // tslint:disable-line:no-any + + // Find the decorator declaration. + if (!node) { + return []; + } + + // Get all the children property assignment of object literals. + const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties + .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter((prop: any) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).text == metadataField; + } + + return false; + }); + + // Get the last node of the array literal. + if (!matchingProperties) { + return []; + } + if (matchingProperties.length == 0) { + // We haven't found the field in the metadata declaration. Insert a new field. + const expr = node as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + const newMetadataProperty = new InsertChange( + ngModulePath, + position, + toInsert + ); + const newMetadataImport = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [newMetadataProperty, newMetadataImport]; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + if (arrLiteral.elements.length == 0) { + // Forward the property. + node = arrLiteral; + } else { + node = arrLiteral.elements; + } + + if (!node) { + console.log( + 'No app module found. Please add your new class to your component.' + ); + + return []; + } + + if (Array.isArray(node)) { + const nodeArray = (node as {}) as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + const effectsModule = nodeArray.find( + node => + (node.getText().includes('EffectsModule.forRoot') && + symbolName.includes('EffectsModule.forRoot')) || + (node.getText().includes('EffectsModule.forFeature') && + symbolName.includes('EffectsModule.forFeature')) + ); + + if (effectsModule && symbolName.includes('EffectsModule')) { + const effectsArgs = (effectsModule as any).arguments.shift(); + + if ( + effectsArgs && + effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression + ) { + const effectsElements = (effectsArgs as ts.ArrayLiteralExpression) + .elements; + const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/); + + let epos; + if (effectsElements.length === 0) { + epos = effectsArgs.getStart() + 1; + return [new InsertChange(ngModulePath, epos, effectsSymbol)]; + } else { + const lastEffect = effectsElements[ + effectsElements.length - 1 + ] as ts.Expression; + epos = lastEffect.getEnd(); + // Get the indentation of the last element, if any. + const text: any = lastEffect.getFullText(source); + + let effectInsert: string; + if (text.match('^\r?\r?\n')) { + effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`; + } else { + effectInsert = `, ${effectsSymbol}`; + } + + return [new InsertChange(ngModulePath, epos, effectInsert)]; + } + } else { + return []; + } + } + } + + node = node[node.length - 1]; + + let toInsert: string; + let position = node.getEnd(); + if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { + // We haven't found the field in the metadata declaration. Insert a new + // field. + const expr = node as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd(); + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match('^\r?\r?\n')) { + toInsert = `,${ + text.match(/^\r?\n\s+/)[0] + }${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } else { + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + if (text.match(/^\r?\n/)) { + toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + const insert = new InsertChange(ngModulePath, position, toInsert); + const importInsert: Change = insertImport( + source, + ngModulePath, + symbolName.replace(/\..*$/, ''), + importPath + ); + + return [insert, importInsert]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'declarations', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addImportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'imports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'providers', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'exports', + classifiedName, + importPath + ); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule( + source: ts.SourceFile, + modulePath: string, + classifiedName: string, + importPath: string +): Change[] { + return _addSymbolToNgModuleMetadata( + source, + modulePath, + 'bootstrap', + classifiedName, + importPath + ); +} diff --git a/modules/store/src/schematics-core/utility/ast-utils_spec.ts b/modules/store/src/schematics-core/utility/ast-utils_spec.ts new file mode 100644 index 0000000000..4290558148 --- /dev/null +++ b/modules/store/src/schematics-core/utility/ast-utils_spec.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { VirtualTree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { getFileContent } from '../testing'; +import { addExportToModule } from './ast-utils'; + +function getTsSource(path: string, content: string): ts.SourceFile { + return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); +} + +function applyChanges( + path: string, + content: string, + changes: Change[] +): string { + const tree = new VirtualTree(); + tree.create(path, content); + const exportRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(exportRecorder); + + return getFileContent(tree, path); +} + +describe('ast utils', () => { + let modulePath: string; + let moduleContent: string; + beforeEach(() => { + modulePath = '/src/app/app.module.ts'; + moduleContent = ` + 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 { } + `; + }); + + it('should add export to module', () => { + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); + + it('should add export to module if not indented', () => { + moduleContent = tags.stripIndent`${moduleContent}`; + const source = getTsSource(modulePath, moduleContent); + const changes = addExportToModule( + source, + modulePath, + 'FooComponent', + './foo.component' + ); + const output = applyChanges(modulePath, moduleContent, changes); + expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); + expect(output).toMatch(/exports: \[FooComponent\]/); + }); +}); diff --git a/modules/store/src/schematics-core/utility/change.ts b/modules/store/src/schematics-core/utility/change.ts new file mode 100644 index 0000000000..22997483b6 --- /dev/null +++ b/modules/store/src/schematics-core/utility/change.ts @@ -0,0 +1,135 @@ +/* istanbul ignore file */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use 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 Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { + return Promise.resolve(); + } +} + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + private toRemove: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor( + public path: string, + private pos: number, + public oldText: string, + public newText: string + ) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject( + new Error(`Invalid replace: "${text}" != "${this.oldText}".`) + ); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/modules/store/src/schematics-core/utility/config.ts b/modules/store/src/schematics-core/utility/config.ts new file mode 100644 index 0000000000..481bfb955f --- /dev/null +++ b/modules/store/src/schematics-core/utility/config.ts @@ -0,0 +1,487 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { experimental } from '@angular-devkit/core'; + +// 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/modules/store/src/schematics-core/utility/find-module.ts b/modules/store/src/schematics-core/utility/find-module.ts new file mode 100644 index 0000000000..7494583b3e --- /dev/null +++ b/modules/store/src/schematics-core/utility/find-module.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Path, join, normalize, relative, strings } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + path?: string; + skipImport?: boolean; +} + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions( + host: Tree, + options: ModuleOptions +): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = + (options.path || '') + + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize('/' + options.path + '/' + options.module); + const moduleBaseName = normalize(modulePath) + .split('/') + .pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter( + p => moduleRe.test(p) && !routingModuleRe.test(p) + ); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error( + 'More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.' + ); + } + + dir = dir.parent; + } + + throw new Error( + 'Could not find an NgModule. Use the skip-import ' + + 'option to skip importing in NgModule.' + ); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative( + normalize(fromParts.join('/')), + normalize(toParts.join('/')) + ); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/modules/store/src/schematics-core/utility/find-module_spec.ts b/modules/store/src/schematics-core/utility/find-module_spec.ts new file mode 100644 index 0000000000..ecdfe1a3f2 --- /dev/null +++ b/modules/store/src/schematics-core/utility/find-module_spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { EmptyTree, Tree } from '@angular-devkit/schematics'; +import { findModule } from './find-module'; + +describe('find-module', () => { + describe('findModule', () => { + let host: Tree; + const modulePath = '/foo/src/app/app.module.ts'; + beforeEach(() => { + host = new EmptyTree(); + host.create(modulePath, 'app module'); + }); + + it('should find a module', () => { + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should not find a module in another sub dir', () => { + host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should ignore routing modules', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo/src/app/bar'); + expect(foundModule).toEqual(modulePath); + }); + + it('should work with weird paths', () => { + host.create('/foo/src/app/app-routing.module.ts', 'app module'); + const foundModule = findModule(host, 'foo//src//app/bar/'); + expect(foundModule).toEqual(modulePath); + }); + + it('should throw if no modules found', () => { + host.create('/foo/src/app/oops.module.ts', 'app module'); + try { + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/More than one module matches/); + } + }); + + it('should throw if two modules found', () => { + try { + host = new EmptyTree(); + findModule(host, 'foo/src/app/bar'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find an NgModule/); + } + }); + }); +}); diff --git a/modules/store/src/schematics-core/utility/ngrx-utils.ts b/modules/store/src/schematics-core/utility/ngrx-utils.ts new file mode 100644 index 0000000000..1873ad0446 --- /dev/null +++ b/modules/store/src/schematics-core/utility/ngrx-utils.ts @@ -0,0 +1,257 @@ +import * as ts from 'typescript'; +import * as stringUtils from './strings'; +import { InsertChange, Change, NoopChange } from './change'; +import { Tree, SchematicsException, Rule } from '@angular-devkit/schematics'; +import { normalize } from '@angular-devkit/core'; +import { buildRelativePath } from './find-module'; +import { insertImport } from './route-utils'; +import { addImportToModule } from './ast-utils'; + +export function addReducerToState(options: any): Rule { + return (host: Tree) => { + if (!options.reducers) { + return host; + } + + const reducersPath = normalize(`/${options.path}/${options.reducers}`); + + if (!host.exists(reducersPath)) { + throw new Error('Specified reducers path does not exist'); + } + + const text = host.read(reducersPath); + if (text === null) { + throw new SchematicsException(`File ${reducersPath} does not exist.`); + } + + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + reducersPath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + + const relativePath = buildRelativePath(reducersPath, reducerPath); + const reducerImport = insertImport( + source, + reducersPath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + + const stateInferfaceInsert = addReducerToStateInferface( + source, + reducersPath, + options + ); + const reducerMapInsert = addReducerToActionReducerMap( + source, + reducersPath, + options + ); + + const changes = [reducerImport, stateInferfaceInsert, reducerMapInsert]; + const recorder = host.beginUpdate(reducersPath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +/** + * Insert the reducer into the first defined top level interface + */ +export function addReducerToStateInferface( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + const stateInterface = source.statements.find( + stm => stm.kind === ts.SyntaxKind.InterfaceDeclaration + ); + let node = stateInterface as ts.Statement; + + if (!node) { + return new NoopChange(); + } + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.State;'; + const expr = node as any; + let position; + let toInsert; + + if (expr.members.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.members[expr.members.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches!.length > 0) { + toInsert = `${matches![1]}${keyInsert}\n`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Insert the reducer into the ActionReducerMap + */ +export function addReducerToActionReducerMap( + source: ts.SourceFile, + reducersPath: string, + options: { name: string } +): Change { + let initializer: any; + const actionReducerMap: any = source.statements + .filter(stm => stm.kind === ts.SyntaxKind.VariableStatement) + .filter((stm: any) => !!stm.declarationList) + .map((stm: any) => { + const { + declarations, + }: { + declarations: ts.SyntaxKind.VariableDeclarationList[]; + } = stm.declarationList; + const variable: any = declarations.find( + (decl: any) => decl.kind === ts.SyntaxKind.VariableDeclaration + ); + const type = variable ? variable.type : {}; + + return { initializer: variable.initializer, type }; + }) + .find(({ type }) => type.typeName.text === 'ActionReducerMap'); + + if (!actionReducerMap || !actionReducerMap.initializer) { + return new NoopChange(); + } + + let node = actionReducerMap.initializer; + + const keyInsert = + stringUtils.camelize(options.name) + + ': from' + + stringUtils.classify(options.name) + + '.reducer,'; + const expr = node as any; + let position; + let toInsert; + + if (expr.properties.length === 0) { + position = expr.getEnd() - 1; + toInsert = ` ${keyInsert}\n`; + } else { + node = expr.properties[expr.properties.length - 1]; + position = node.getEnd() + 1; + // Get the indentation of the last element, if any. + const text = node.getFullText(source); + const matches = text.match(/^\r?\n+(\s*)/); + + if (matches.length > 0) { + toInsert = `\n${matches![1]}${keyInsert}`; + } else { + toInsert = `\n${keyInsert}`; + } + } + + return new InsertChange(reducersPath, position, toInsert); +} + +/** + * Add reducer feature to NgModule + */ +export function addReducerImportToNgModule(options: any): 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.`); + } + const sourceText = text.toString('utf-8'); + + const source = ts.createSourceFile( + modulePath, + sourceText, + ts.ScriptTarget.Latest, + true + ); + + const commonImports = [ + insertImport(source, modulePath, 'StoreModule', '@ngrx/store'), + ]; + + const reducerPath = + `/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + (options.group ? 'reducers/' : '') + + stringUtils.dasherize(options.name) + + '.reducer'; + const relativePath = buildRelativePath(modulePath, reducerPath); + const reducerImport = insertImport( + source, + modulePath, + `* as from${stringUtils.classify(options.name)}`, + relativePath, + true + ); + const [storeNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreModule.forFeature('${stringUtils.camelize( + options.name + )}', from${stringUtils.classify(options.name)}.reducer)`, + relativePath + ); + const changes = [...commonImports, reducerImport, storeNgModuleImport]; + const recorder = host.beginUpdate(modulePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + + return host; + }; +} + +export function omit( + object: T, + keyToRemove: keyof T +): Partial { + return Object.keys(object) + .filter(key => key !== keyToRemove) + .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); +} diff --git a/modules/store/src/schematics-core/utility/project.ts b/modules/store/src/schematics-core/utility/project.ts new file mode 100644 index 0000000000..c9aeda34a8 --- /dev/null +++ b/modules/store/src/schematics-core/utility/project.ts @@ -0,0 +1,28 @@ +import { getWorkspace } from './config'; +import { Tree } from '@angular-devkit/schematics'; + +export function getProjectPath( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (project.root.substr(-1) === '/') { + project.root = project.root.substr(0, project.root.length - 1); + } + + if (options.path === undefined) { + const projectDirName = + project.projectType === 'application' ? 'app' : 'lib'; + + return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`; + } + + return options.path; +} diff --git a/modules/store/src/schematics-core/utility/route-utils.ts b/modules/store/src/schematics-core/utility/route-utils.ts new file mode 100644 index 0000000000..d25cc059a8 --- /dev/null +++ b/modules/store/src/schematics-core/utility/route-utils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of 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 { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + +/** + * Add Import `import { symbolName } from fileName` if the import doesn't exit + * already. Assumes fileToEdit can be resolved and accessed. + * @param fileToEdit (file we want to add import to) + * @param symbolName (item to import) + * @param fileName (path to the file) + * @param isDefault (if true, import follows style for importing default exports) + * @return Change + */ + +export function insertImport( + source: ts.SourceFile, + fileToEdit: string, + symbolName: string, + fileName: string, + isDefault = false +): Change { + const rootNode = source; + const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + + // get nodes that map to import statements from the file fileName + const relevantImports = allImports.filter(node => { + // StringLiteral of the ImportDeclaration is the import file (fileName in this case). + const importFiles = node + .getChildren() + .filter(child => child.kind === ts.SyntaxKind.StringLiteral) + .map(n => (n as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply( + imports, + findNodes(n, ts.SyntaxKind.Identifier) + ); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter( + n => (n as ts.Identifier).text === symbolName + ); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes( + relevantImports[0], + ts.SyntaxKind.CloseBraceToken + )[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence( + imports, + `, ${symbolName}`, + fileToEdit, + fallbackPos + ); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter( + n => n.getText() === 'use strict' + ); + let fallbackPos = 0; + if (useStrict.length > 0) { + fallbackPos = useStrict[0].end; + } + const open = isDefault ? '' : '{ '; + const close = isDefault ? '' : ' }'; + // if there are no imports or 'use strict' statement, insert import at beginning of file + const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; + const separator = insertAtBeginning ? '' : ';\n'; + const toInsert = + `${separator}import ${open}${symbolName}${close}` + + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + + return insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral + ); +} diff --git a/modules/store/src/schematics-core/utility/strings.ts b/modules/store/src/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..b2832e56b7 --- /dev/null +++ b/modules/store/src/schematics-core/utility/strings.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + + @method decamelize + @param {String} str The string to decamelize. + @return {String} the decamelized string. + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + + @method dasherize + @param {String} str The string to dasherize. + @return {String} the dasherized string. + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + + @method camelize + @param {String} str The string to camelize. + @return {String} the camelized string. + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + + @method classify + @param {String} str the string to classify + @return {String} the classified string + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + + @method underscore + @param {String} str The string to underscore. + @return {String} the underscored string. + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + + @method capitalize + @param {String} str The string to capitalize. + @return {String} The capitalized string. + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/store/src/schematics-core/utility/update.ts b/modules/store/src/schematics-core/utility/update.ts new file mode 100644 index 0000000000..352f30b027 --- /dev/null +++ b/modules/store/src/schematics-core/utility/update.ts @@ -0,0 +1,47 @@ +import { + Rule, + SchematicContext, + Tree, + SchematicsException, + chain, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +export function updatePackage(name: string): Rule { + return (tree: Tree, context: SchematicContext) => { + const pkgPath = '/package.json'; + const buffer = tree.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'); + } + + const dependencyCategories = ['dependencies', 'devDependencies']; + + dependencyCategories.forEach(category => { + const packageName = `@ngrx/${name}`; + + if (pkg[category] && pkg[category][packageName]) { + const firstChar = pkg[category][packageName][0]; + const suffix = match(firstChar, '^') || match(firstChar, '~'); + + // TODO: remove beta + pkg[category][packageName] = `${suffix}6.0.0-beta.2`; + } + }); + + tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + context.addTask(new NodePackageInstallTask()); + + return tree; + }; +} + +function match(value: string, test: string) { + return value === test ? test : ''; +} diff --git a/package.json b/package.json index 0db640dcab..182c41b509 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "bazel build ...", "deploy:builds": "ts-node ./build/deploy-build.ts", "test:unit": "node ./tests.js", - "test": "yarn copy:schematics && nyc yarn run test:unit", + "test": "nyc yarn run test:unit", "clean": "git clean -xdf && yarn && yarn run bootstrap", "cli": "ng", "coverage:html": "nyc report --reporter=html", @@ -16,8 +16,8 @@ "example:start": "yarn run cli serve", "example:start:aot": "yarn run cli serve -prod", "example:test": "jest --watch", - "example:build:prod": "yarn build && yarn run copy:dist && yarn cli build --prod --base-href \"/platform/example-app/\" --output-path \"./example-dist/example-app\"", - "ci": "yarn run build && yarn run test && nyc report --reporter=text-lcov | coveralls", + "example:build:prod": "yarn cli build --prod --base-href \"/platform/example-app/\" --output-path \"./example-dist/example-app\"", + "ci": "yarn run test && nyc report --reporter=text-lcov | coveralls", "prettier": "prettier --write \"**/*.ts\"", "watch:tests": "chokidar 'modules/**/*.ts' --initial -c 'nyc --reporter=text --reporter=html yarn run test:unit'", "postinstall": "node tools/rxjs-patch-pr3322.js && opencollective postinstall", From dc31a1dbeac6739eddfb3151e3906c24808de95e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 11 May 2018 11:44:13 -0500 Subject: [PATCH 2/4] feat: Add ng update support to ngrx packages --- .gitignore | 4 +- build/config.ts | 2 +- build/copy-schematics-core.ts | 15 ++ build/tasks.ts | 31 +++- modules/effects/BUILD | 4 + modules/effects/migrations/6_0_0/index.ts | 2 +- modules/effects/migrations/BUILD | 31 ++++ modules/effects/schematics-core/BUILD | 26 +++ .../{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../schematics-core/utility/strings.ts | 24 --- .../schematics-core/utility/update.ts | 0 .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- modules/entity/BUILD | 4 + modules/entity/migrations/6_0_0/index.ts | 2 +- modules/entity/migrations/BUILD | 31 ++++ modules/entity/schematics-core/BUILD | 26 +++ .../entity/{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../schematics-core/utility/strings.ts | 24 --- .../schematics-core/utility/update.ts | 0 .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- modules/router-store/BUILD | 4 + .../router-store/migrations/6_0_0/index.ts | 2 +- modules/router-store/migrations/BUILD | 31 ++++ modules/router-store/schematics-core/BUILD | 26 +++ .../{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../schematics-core/utility/strings.ts | 24 --- .../schematics-core/utility/update.ts | 0 .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- modules/schematics-core/utility/strings.ts | 24 --- modules/schematics/BUILD | 7 + modules/schematics/migrations/6_0_0/index.ts | 2 +- modules/schematics/migrations/BUILD | 31 ++++ modules/schematics/schematics-core/BUILD | 26 +++ .../{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../schematics-core/utility/strings.ts | 24 --- .../schematics-core/utility/update.ts | 0 modules/schematics/src/action/index.ts | 2 +- modules/schematics/src/container/index.ts | 2 +- modules/schematics/src/effect/index.ts | 2 +- modules/schematics/src/entity/index.ts | 2 +- modules/schematics/src/reducer/index.ts | 2 +- .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- modules/schematics/src/store/index.ts | 2 +- modules/store-devtools/BUILD | 4 + .../store-devtools/migrations/6_0_0/index.ts | 2 +- modules/store-devtools/migrations/BUILD | 31 ++++ modules/store-devtools/schematics-core/BUILD | 26 +++ .../{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../schematics-core/utility/strings.ts | 128 +++++++++++++++ .../schematics-core/utility/update.ts | 0 .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- .../src/schematics-core/utility/strings.ts | 152 ------------------ modules/store/BUILD | 4 + modules/store/migrations/6_0_0/index.spec.ts | 4 +- modules/store/migrations/6_0_0/index.ts | 2 +- modules/store/migrations/BUILD | 31 ++++ modules/store/schematics-core/BUILD | 26 +++ .../store/{src => }/schematics-core/index.ts | 0 .../schematics-core/utility/ast-utils.ts | 0 .../schematics-core/utility/change.ts | 0 .../schematics-core/utility/config.ts | 0 .../schematics-core/utility/find-module.ts | 0 .../schematics-core/utility/ngrx-utils.ts | 0 .../schematics-core/utility/project.ts | 0 .../schematics-core/utility/route-utils.ts | 0 .../store/schematics-core/utility/strings.ts | 128 +++++++++++++++ .../schematics-core/utility/update.ts | 0 .../testing/create-app-module.ts | 60 ------- .../schematics-core/testing/create-package.ts | 26 --- .../testing/create-reducers.ts | 34 ---- .../testing/create-workspace.ts | 55 ------- .../testing/get-file-content.ts | 11 -- .../src/schematics-core/testing/index.ts | 4 - .../src/schematics-core/testing/update.ts | 2 - .../schematics-core/utility/ast-utils_spec.ts | 87 ---------- .../utility/find-module_spec.ts | 63 -------- .../src/schematics-core/utility/strings.ts | 152 ------------------ package.json | 4 +- tools/defaults.bzl | 8 +- tsconfig.json | 14 +- yarn.lock | 6 + 158 files changed, 717 insertions(+), 2646 deletions(-) create mode 100644 build/copy-schematics-core.ts create mode 100644 modules/effects/migrations/BUILD create mode 100644 modules/effects/schematics-core/BUILD rename modules/effects/{src => }/schematics-core/index.ts (100%) rename modules/effects/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/effects/{src => }/schematics-core/utility/change.ts (100%) rename modules/effects/{src => }/schematics-core/utility/config.ts (100%) rename modules/effects/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/effects/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/effects/{src => }/schematics-core/utility/project.ts (100%) rename modules/effects/{src => }/schematics-core/utility/route-utils.ts (100%) rename modules/{router-store/src => effects}/schematics-core/utility/strings.ts (84%) rename modules/effects/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/effects/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/effects/src/schematics-core/testing/create-package.ts delete mode 100644 modules/effects/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/effects/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/effects/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/effects/src/schematics-core/testing/index.ts delete mode 100644 modules/effects/src/schematics-core/testing/update.ts delete mode 100644 modules/effects/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/effects/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/entity/migrations/BUILD create mode 100644 modules/entity/schematics-core/BUILD rename modules/entity/{src => }/schematics-core/index.ts (100%) rename modules/entity/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/entity/{src => }/schematics-core/utility/change.ts (100%) rename modules/entity/{src => }/schematics-core/utility/config.ts (100%) rename modules/entity/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/entity/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/entity/{src => }/schematics-core/utility/project.ts (100%) rename modules/entity/{src => }/schematics-core/utility/route-utils.ts (100%) rename modules/entity/{src => }/schematics-core/utility/strings.ts (84%) rename modules/entity/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/entity/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/entity/src/schematics-core/testing/create-package.ts delete mode 100644 modules/entity/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/entity/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/entity/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/entity/src/schematics-core/testing/index.ts delete mode 100644 modules/entity/src/schematics-core/testing/update.ts delete mode 100644 modules/entity/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/entity/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/router-store/migrations/BUILD create mode 100644 modules/router-store/schematics-core/BUILD rename modules/router-store/{src => }/schematics-core/index.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/change.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/config.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/project.ts (100%) rename modules/router-store/{src => }/schematics-core/utility/route-utils.ts (100%) rename modules/{effects/src => router-store}/schematics-core/utility/strings.ts (84%) rename modules/router-store/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/router-store/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/router-store/src/schematics-core/testing/create-package.ts delete mode 100644 modules/router-store/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/router-store/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/router-store/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/router-store/src/schematics-core/testing/index.ts delete mode 100644 modules/router-store/src/schematics-core/testing/update.ts delete mode 100644 modules/router-store/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/router-store/src/schematics-core/utility/find-module_spec.ts delete mode 100644 modules/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/schematics-core/utility/find-module_spec.ts create mode 100644 modules/schematics/migrations/BUILD create mode 100644 modules/schematics/schematics-core/BUILD rename modules/schematics/{src => }/schematics-core/index.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/change.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/config.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/project.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/route-utils.ts (100%) rename modules/schematics/{src => }/schematics-core/utility/strings.ts (84%) rename modules/schematics/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/schematics/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/schematics/src/schematics-core/testing/create-package.ts delete mode 100644 modules/schematics/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/schematics/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/schematics/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/schematics/src/schematics-core/testing/index.ts delete mode 100644 modules/schematics/src/schematics-core/testing/update.ts delete mode 100644 modules/schematics/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/schematics/src/schematics-core/utility/find-module_spec.ts create mode 100644 modules/store-devtools/migrations/BUILD create mode 100644 modules/store-devtools/schematics-core/BUILD rename modules/store-devtools/{src => }/schematics-core/index.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/change.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/config.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/project.ts (100%) rename modules/store-devtools/{src => }/schematics-core/utility/route-utils.ts (100%) create mode 100644 modules/store-devtools/schematics-core/utility/strings.ts rename modules/store-devtools/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/store-devtools/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/create-package.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/index.ts delete mode 100644 modules/store-devtools/src/schematics-core/testing/update.ts delete mode 100644 modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/store-devtools/src/schematics-core/utility/find-module_spec.ts delete mode 100644 modules/store-devtools/src/schematics-core/utility/strings.ts create mode 100644 modules/store/migrations/BUILD create mode 100644 modules/store/schematics-core/BUILD rename modules/store/{src => }/schematics-core/index.ts (100%) rename modules/store/{src => }/schematics-core/utility/ast-utils.ts (100%) rename modules/store/{src => }/schematics-core/utility/change.ts (100%) rename modules/store/{src => }/schematics-core/utility/config.ts (100%) rename modules/store/{src => }/schematics-core/utility/find-module.ts (100%) rename modules/store/{src => }/schematics-core/utility/ngrx-utils.ts (100%) rename modules/store/{src => }/schematics-core/utility/project.ts (100%) rename modules/store/{src => }/schematics-core/utility/route-utils.ts (100%) create mode 100644 modules/store/schematics-core/utility/strings.ts rename modules/store/{src => }/schematics-core/utility/update.ts (100%) delete mode 100644 modules/store/src/schematics-core/testing/create-app-module.ts delete mode 100644 modules/store/src/schematics-core/testing/create-package.ts delete mode 100644 modules/store/src/schematics-core/testing/create-reducers.ts delete mode 100644 modules/store/src/schematics-core/testing/create-workspace.ts delete mode 100644 modules/store/src/schematics-core/testing/get-file-content.ts delete mode 100644 modules/store/src/schematics-core/testing/index.ts delete mode 100644 modules/store/src/schematics-core/testing/update.ts delete mode 100644 modules/store/src/schematics-core/utility/ast-utils_spec.ts delete mode 100644 modules/store/src/schematics-core/utility/find-module_spec.ts delete mode 100644 modules/store/src/schematics-core/utility/strings.ts diff --git a/.gitignore b/.gitignore index 4c81f88397..dbcfa5eccf 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,6 @@ tmp example-dist/ -*.tgz \ No newline at end of file +*.tgz +modules/*/schematics-core/testing +!modules/schematics-core/testing \ No newline at end of file diff --git a/build/config.ts b/build/config.ts index dca749aa95..dca0da8283 100644 --- a/build/config.ts +++ b/build/config.ts @@ -9,7 +9,7 @@ export interface Config { scope: string; } -const modulesDir = './modules/'; +export const modulesDir = './modules/'; export const packages: PackageDescription[] = fs .readdirSync(modulesDir) .filter(path => { diff --git a/build/copy-schematics-core.ts b/build/copy-schematics-core.ts new file mode 100644 index 0000000000..73d57be872 --- /dev/null +++ b/build/copy-schematics-core.ts @@ -0,0 +1,15 @@ +import * as tasks from './tasks'; +import { createBuilder } from './util'; +import { packages } from './config'; + +const copySchematics = createBuilder([ + ['Copy Schematics Core Files', tasks.copySchematicsCore], +]); + +copySchematics({ + scope: '@ngrx', + packages, +}).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/build/tasks.ts b/build/tasks.ts index 57ef68fb11..1d0850ae7d 100644 --- a/build/tasks.ts +++ b/build/tasks.ts @@ -1,5 +1,34 @@ -import { Config } from './config'; +import { Config, modulesDir } from './config'; import * as util from './util'; +import * as fs from 'fs'; +import { ncp } from 'ncp'; + +/** + * + * Copies the schematics-core package into any package that provides + * schematics or migrations + */ +export async function copySchematicsCore(config: Config) { + (ncp as any).limit = 1; + for (let pkg of util.getTopLevelPackages(config)) { + const packageJson = fs + .readFileSync(`${modulesDir}${pkg}/package.json`) + .toString('utf-8'); + const pkgConfig = JSON.parse(packageJson); + + if (pkgConfig.schematics || pkgConfig['ng-update'].migrations) { + ncp( + `${modulesDir}/schematics-core`, + `${modulesDir}/${pkg}/schematics-core`, + function(err: any) { + if (err) { + return console.error(err); + } + } + ); + } + } +} /** * Deploy build artifacts to repos diff --git a/modules/effects/BUILD b/modules/effects/BUILD index 2afd4b9cf3..3684c5f53a 100644 --- a/modules/effects/BUILD +++ b/modules/effects/BUILD @@ -27,4 +27,8 @@ ng_package( ":effects", "//modules/effects/testing", ], + packages = [ + "//modules/effects/migrations:npm_package", + "//modules/effects/schematics-core:npm_package", + ], ) diff --git a/modules/effects/migrations/6_0_0/index.ts b/modules/effects/migrations/6_0_0/index.ts index e92e75a56e..6811b2cceb 100644 --- a/modules/effects/migrations/6_0_0/index.ts +++ b/modules/effects/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/effects/schematics-core'; export default function(): Rule { return updatePackage('effects'); diff --git a/modules/effects/migrations/BUILD b/modules/effects/migrations/BUILD new file mode 100644 index 0000000000..3c392f5733 --- /dev/null +++ b/modules/effects/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/effects/migrations", + deps = [ + "//modules/effects/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/effects/schematics-core/BUILD b/modules/effects/schematics-core/BUILD new file mode 100644 index 0000000000..a3ad259df2 --- /dev/null +++ b/modules/effects/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/effects/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/effects/src/schematics-core/index.ts b/modules/effects/schematics-core/index.ts similarity index 100% rename from modules/effects/src/schematics-core/index.ts rename to modules/effects/schematics-core/index.ts diff --git a/modules/effects/src/schematics-core/utility/ast-utils.ts b/modules/effects/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/ast-utils.ts rename to modules/effects/schematics-core/utility/ast-utils.ts diff --git a/modules/effects/src/schematics-core/utility/change.ts b/modules/effects/schematics-core/utility/change.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/change.ts rename to modules/effects/schematics-core/utility/change.ts diff --git a/modules/effects/src/schematics-core/utility/config.ts b/modules/effects/schematics-core/utility/config.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/config.ts rename to modules/effects/schematics-core/utility/config.ts diff --git a/modules/effects/src/schematics-core/utility/find-module.ts b/modules/effects/schematics-core/utility/find-module.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/find-module.ts rename to modules/effects/schematics-core/utility/find-module.ts diff --git a/modules/effects/src/schematics-core/utility/ngrx-utils.ts b/modules/effects/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/ngrx-utils.ts rename to modules/effects/schematics-core/utility/ngrx-utils.ts diff --git a/modules/effects/src/schematics-core/utility/project.ts b/modules/effects/schematics-core/utility/project.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/project.ts rename to modules/effects/schematics-core/utility/project.ts diff --git a/modules/effects/src/schematics-core/utility/route-utils.ts b/modules/effects/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/route-utils.ts rename to modules/effects/schematics-core/utility/route-utils.ts diff --git a/modules/router-store/src/schematics-core/utility/strings.ts b/modules/effects/schematics-core/utility/strings.ts similarity index 84% rename from modules/router-store/src/schematics-core/utility/strings.ts rename to modules/effects/schematics-core/utility/strings.ts index b2832e56b7..dae37e563c 100644 --- a/modules/router-store/src/schematics-core/utility/strings.ts +++ b/modules/effects/schematics-core/utility/strings.ts @@ -20,10 +20,6 @@ const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; decamelize('css-class-name'); // 'css-class-name' decamelize('my favorite items'); // 'my favorite items' ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. */ export function decamelize(str: string): string { return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); @@ -38,10 +34,6 @@ export function decamelize(str: string): string { dasherize('css-class-name'); // 'css-class-name' dasherize('my favorite items'); // 'my-favorite-items' ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. */ export function dasherize(str?: string): string { return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); @@ -57,10 +49,6 @@ export function dasherize(str?: string): string { camelize('my favorite items'); // 'myFavoriteItems' camelize('My Favorite Items'); // 'myFavoriteItems' ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. */ export function camelize(str: string): string { return str @@ -82,10 +70,6 @@ export function camelize(str: string): string { 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string */ export function classify(str: string): string { return str @@ -104,10 +88,6 @@ export function classify(str: string): string { 'css-class-name'.underscore(); // 'css_class_name' 'my favorite items'.underscore(); // 'my_favorite_items' ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. */ export function underscore(str: string): string { return str @@ -125,10 +105,6 @@ export function underscore(str: string): string { 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. */ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); diff --git a/modules/effects/src/schematics-core/utility/update.ts b/modules/effects/schematics-core/utility/update.ts similarity index 100% rename from modules/effects/src/schematics-core/utility/update.ts rename to modules/effects/schematics-core/utility/update.ts diff --git a/modules/effects/src/schematics-core/testing/create-app-module.ts b/modules/effects/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/effects/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/effects/src/schematics-core/testing/create-package.ts b/modules/effects/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/effects/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/effects/src/schematics-core/testing/create-reducers.ts b/modules/effects/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/effects/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/effects/src/schematics-core/testing/create-workspace.ts b/modules/effects/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/effects/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/effects/src/schematics-core/testing/get-file-content.ts b/modules/effects/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/effects/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/effects/src/schematics-core/testing/index.ts b/modules/effects/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/effects/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/effects/src/schematics-core/testing/update.ts b/modules/effects/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/effects/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/effects/src/schematics-core/utility/ast-utils_spec.ts b/modules/effects/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/effects/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/effects/src/schematics-core/utility/find-module_spec.ts b/modules/effects/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/effects/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/entity/BUILD b/modules/entity/BUILD index 6f4b2cdbed..ac72887e63 100644 --- a/modules/entity/BUILD +++ b/modules/entity/BUILD @@ -23,4 +23,8 @@ ng_package( deps = [ ":entity", ], + packages = [ + "//modules/entity/migrations:npm_package", + "//modules/entity/schematics-core:npm_package", + ], ) diff --git a/modules/entity/migrations/6_0_0/index.ts b/modules/entity/migrations/6_0_0/index.ts index b344fe4a5a..cde64d64ab 100644 --- a/modules/entity/migrations/6_0_0/index.ts +++ b/modules/entity/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/entity/schematics-core'; export default function(): Rule { return updatePackage('entity'); diff --git a/modules/entity/migrations/BUILD b/modules/entity/migrations/BUILD new file mode 100644 index 0000000000..eecfe74d4c --- /dev/null +++ b/modules/entity/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/entity/migrations", + deps = [ + "//modules/entity/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/entity/schematics-core/BUILD b/modules/entity/schematics-core/BUILD new file mode 100644 index 0000000000..f887538800 --- /dev/null +++ b/modules/entity/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/entity/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/entity/src/schematics-core/index.ts b/modules/entity/schematics-core/index.ts similarity index 100% rename from modules/entity/src/schematics-core/index.ts rename to modules/entity/schematics-core/index.ts diff --git a/modules/entity/src/schematics-core/utility/ast-utils.ts b/modules/entity/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/ast-utils.ts rename to modules/entity/schematics-core/utility/ast-utils.ts diff --git a/modules/entity/src/schematics-core/utility/change.ts b/modules/entity/schematics-core/utility/change.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/change.ts rename to modules/entity/schematics-core/utility/change.ts diff --git a/modules/entity/src/schematics-core/utility/config.ts b/modules/entity/schematics-core/utility/config.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/config.ts rename to modules/entity/schematics-core/utility/config.ts diff --git a/modules/entity/src/schematics-core/utility/find-module.ts b/modules/entity/schematics-core/utility/find-module.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/find-module.ts rename to modules/entity/schematics-core/utility/find-module.ts diff --git a/modules/entity/src/schematics-core/utility/ngrx-utils.ts b/modules/entity/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/ngrx-utils.ts rename to modules/entity/schematics-core/utility/ngrx-utils.ts diff --git a/modules/entity/src/schematics-core/utility/project.ts b/modules/entity/schematics-core/utility/project.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/project.ts rename to modules/entity/schematics-core/utility/project.ts diff --git a/modules/entity/src/schematics-core/utility/route-utils.ts b/modules/entity/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/route-utils.ts rename to modules/entity/schematics-core/utility/route-utils.ts diff --git a/modules/entity/src/schematics-core/utility/strings.ts b/modules/entity/schematics-core/utility/strings.ts similarity index 84% rename from modules/entity/src/schematics-core/utility/strings.ts rename to modules/entity/schematics-core/utility/strings.ts index b2832e56b7..dae37e563c 100644 --- a/modules/entity/src/schematics-core/utility/strings.ts +++ b/modules/entity/schematics-core/utility/strings.ts @@ -20,10 +20,6 @@ const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; decamelize('css-class-name'); // 'css-class-name' decamelize('my favorite items'); // 'my favorite items' ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. */ export function decamelize(str: string): string { return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); @@ -38,10 +34,6 @@ export function decamelize(str: string): string { dasherize('css-class-name'); // 'css-class-name' dasherize('my favorite items'); // 'my-favorite-items' ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. */ export function dasherize(str?: string): string { return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); @@ -57,10 +49,6 @@ export function dasherize(str?: string): string { camelize('my favorite items'); // 'myFavoriteItems' camelize('My Favorite Items'); // 'myFavoriteItems' ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. */ export function camelize(str: string): string { return str @@ -82,10 +70,6 @@ export function camelize(str: string): string { 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string */ export function classify(str: string): string { return str @@ -104,10 +88,6 @@ export function classify(str: string): string { 'css-class-name'.underscore(); // 'css_class_name' 'my favorite items'.underscore(); // 'my_favorite_items' ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. */ export function underscore(str: string): string { return str @@ -125,10 +105,6 @@ export function underscore(str: string): string { 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. */ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); diff --git a/modules/entity/src/schematics-core/utility/update.ts b/modules/entity/schematics-core/utility/update.ts similarity index 100% rename from modules/entity/src/schematics-core/utility/update.ts rename to modules/entity/schematics-core/utility/update.ts diff --git a/modules/entity/src/schematics-core/testing/create-app-module.ts b/modules/entity/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/entity/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/entity/src/schematics-core/testing/create-package.ts b/modules/entity/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/entity/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/entity/src/schematics-core/testing/create-reducers.ts b/modules/entity/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/entity/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/entity/src/schematics-core/testing/create-workspace.ts b/modules/entity/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/entity/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/entity/src/schematics-core/testing/get-file-content.ts b/modules/entity/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/entity/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/entity/src/schematics-core/testing/index.ts b/modules/entity/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/entity/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/entity/src/schematics-core/testing/update.ts b/modules/entity/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/entity/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/entity/src/schematics-core/utility/ast-utils_spec.ts b/modules/entity/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/entity/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/entity/src/schematics-core/utility/find-module_spec.ts b/modules/entity/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/entity/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/router-store/BUILD b/modules/router-store/BUILD index 5918423c6f..bd4c2db856 100644 --- a/modules/router-store/BUILD +++ b/modules/router-store/BUILD @@ -24,4 +24,8 @@ ng_package( deps = [ ":router-store", ], + packages = [ + "//modules/router-store/migrations:npm_package", + "//modules/router-store/schematics-core:npm_package", + ], ) diff --git a/modules/router-store/migrations/6_0_0/index.ts b/modules/router-store/migrations/6_0_0/index.ts index e7790ce765..0c5a93d8e4 100644 --- a/modules/router-store/migrations/6_0_0/index.ts +++ b/modules/router-store/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/router-store/schematics-core'; export default function(): Rule { return updatePackage('router-store'); diff --git a/modules/router-store/migrations/BUILD b/modules/router-store/migrations/BUILD new file mode 100644 index 0000000000..3cc1026d5d --- /dev/null +++ b/modules/router-store/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/router-store/migrations", + deps = [ + "//modules/router-store/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/router-store/schematics-core/BUILD b/modules/router-store/schematics-core/BUILD new file mode 100644 index 0000000000..ceab380ddc --- /dev/null +++ b/modules/router-store/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/router-store/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/router-store/src/schematics-core/index.ts b/modules/router-store/schematics-core/index.ts similarity index 100% rename from modules/router-store/src/schematics-core/index.ts rename to modules/router-store/schematics-core/index.ts diff --git a/modules/router-store/src/schematics-core/utility/ast-utils.ts b/modules/router-store/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/ast-utils.ts rename to modules/router-store/schematics-core/utility/ast-utils.ts diff --git a/modules/router-store/src/schematics-core/utility/change.ts b/modules/router-store/schematics-core/utility/change.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/change.ts rename to modules/router-store/schematics-core/utility/change.ts diff --git a/modules/router-store/src/schematics-core/utility/config.ts b/modules/router-store/schematics-core/utility/config.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/config.ts rename to modules/router-store/schematics-core/utility/config.ts diff --git a/modules/router-store/src/schematics-core/utility/find-module.ts b/modules/router-store/schematics-core/utility/find-module.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/find-module.ts rename to modules/router-store/schematics-core/utility/find-module.ts diff --git a/modules/router-store/src/schematics-core/utility/ngrx-utils.ts b/modules/router-store/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/ngrx-utils.ts rename to modules/router-store/schematics-core/utility/ngrx-utils.ts diff --git a/modules/router-store/src/schematics-core/utility/project.ts b/modules/router-store/schematics-core/utility/project.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/project.ts rename to modules/router-store/schematics-core/utility/project.ts diff --git a/modules/router-store/src/schematics-core/utility/route-utils.ts b/modules/router-store/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/route-utils.ts rename to modules/router-store/schematics-core/utility/route-utils.ts diff --git a/modules/effects/src/schematics-core/utility/strings.ts b/modules/router-store/schematics-core/utility/strings.ts similarity index 84% rename from modules/effects/src/schematics-core/utility/strings.ts rename to modules/router-store/schematics-core/utility/strings.ts index b2832e56b7..dae37e563c 100644 --- a/modules/effects/src/schematics-core/utility/strings.ts +++ b/modules/router-store/schematics-core/utility/strings.ts @@ -20,10 +20,6 @@ const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; decamelize('css-class-name'); // 'css-class-name' decamelize('my favorite items'); // 'my favorite items' ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. */ export function decamelize(str: string): string { return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); @@ -38,10 +34,6 @@ export function decamelize(str: string): string { dasherize('css-class-name'); // 'css-class-name' dasherize('my favorite items'); // 'my-favorite-items' ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. */ export function dasherize(str?: string): string { return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); @@ -57,10 +49,6 @@ export function dasherize(str?: string): string { camelize('my favorite items'); // 'myFavoriteItems' camelize('My Favorite Items'); // 'myFavoriteItems' ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. */ export function camelize(str: string): string { return str @@ -82,10 +70,6 @@ export function camelize(str: string): string { 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string */ export function classify(str: string): string { return str @@ -104,10 +88,6 @@ export function classify(str: string): string { 'css-class-name'.underscore(); // 'css_class_name' 'my favorite items'.underscore(); // 'my_favorite_items' ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. */ export function underscore(str: string): string { return str @@ -125,10 +105,6 @@ export function underscore(str: string): string { 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. */ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); diff --git a/modules/router-store/src/schematics-core/utility/update.ts b/modules/router-store/schematics-core/utility/update.ts similarity index 100% rename from modules/router-store/src/schematics-core/utility/update.ts rename to modules/router-store/schematics-core/utility/update.ts diff --git a/modules/router-store/src/schematics-core/testing/create-app-module.ts b/modules/router-store/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/router-store/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/router-store/src/schematics-core/testing/create-package.ts b/modules/router-store/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/router-store/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/router-store/src/schematics-core/testing/create-reducers.ts b/modules/router-store/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/router-store/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/router-store/src/schematics-core/testing/create-workspace.ts b/modules/router-store/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/router-store/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/router-store/src/schematics-core/testing/get-file-content.ts b/modules/router-store/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/router-store/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/router-store/src/schematics-core/testing/index.ts b/modules/router-store/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/router-store/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/router-store/src/schematics-core/testing/update.ts b/modules/router-store/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/router-store/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts b/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/router-store/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/router-store/src/schematics-core/utility/find-module_spec.ts b/modules/router-store/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/router-store/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/schematics-core/utility/ast-utils_spec.ts b/modules/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/schematics-core/utility/find-module_spec.ts b/modules/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/schematics-core/utility/strings.ts b/modules/schematics-core/utility/strings.ts index b2832e56b7..dae37e563c 100644 --- a/modules/schematics-core/utility/strings.ts +++ b/modules/schematics-core/utility/strings.ts @@ -20,10 +20,6 @@ const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; decamelize('css-class-name'); // 'css-class-name' decamelize('my favorite items'); // 'my favorite items' ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. */ export function decamelize(str: string): string { return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); @@ -38,10 +34,6 @@ export function decamelize(str: string): string { dasherize('css-class-name'); // 'css-class-name' dasherize('my favorite items'); // 'my-favorite-items' ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. */ export function dasherize(str?: string): string { return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); @@ -57,10 +49,6 @@ export function dasherize(str?: string): string { camelize('my favorite items'); // 'myFavoriteItems' camelize('My Favorite Items'); // 'myFavoriteItems' ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. */ export function camelize(str: string): string { return str @@ -82,10 +70,6 @@ export function camelize(str: string): string { 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string */ export function classify(str: string): string { return str @@ -104,10 +88,6 @@ export function classify(str: string): string { 'css-class-name'.underscore(); // 'css_class_name' 'my favorite items'.underscore(); // 'my_favorite_items' ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. */ export function underscore(str: string): string { return str @@ -125,10 +105,6 @@ export function underscore(str: string): string { 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. */ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); diff --git a/modules/schematics/BUILD b/modules/schematics/BUILD index aabb1e8c25..c30b36a177 100644 --- a/modules/schematics/BUILD +++ b/modules/schematics/BUILD @@ -16,6 +16,9 @@ ts_library( ), module_name = "@ngrx/schematics", tsconfig = ":tsconfig-build.json", + deps = [ + "//modules/schematics/schematics-core" + ] ) npm_package( @@ -32,4 +35,8 @@ npm_package( deps = [ ":schematics", ], + packages = [ + "//modules/schematics/migrations:npm_package", + "//modules/schematics/schematics-core:npm_package", + ], ) diff --git a/modules/schematics/migrations/6_0_0/index.ts b/modules/schematics/migrations/6_0_0/index.ts index f2329b24d6..66b7b33e47 100644 --- a/modules/schematics/migrations/6_0_0/index.ts +++ b/modules/schematics/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/schematics/schematics-core'; export default function(): Rule { return updatePackage('schematics'); diff --git a/modules/schematics/migrations/BUILD b/modules/schematics/migrations/BUILD new file mode 100644 index 0000000000..99b9cff104 --- /dev/null +++ b/modules/schematics/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/schematics/migrations", + deps = [ + "//modules/schematics/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/schematics/schematics-core/BUILD b/modules/schematics/schematics-core/BUILD new file mode 100644 index 0000000000..97bdee518a --- /dev/null +++ b/modules/schematics/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/schematics/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/schematics/src/schematics-core/index.ts b/modules/schematics/schematics-core/index.ts similarity index 100% rename from modules/schematics/src/schematics-core/index.ts rename to modules/schematics/schematics-core/index.ts diff --git a/modules/schematics/src/schematics-core/utility/ast-utils.ts b/modules/schematics/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/ast-utils.ts rename to modules/schematics/schematics-core/utility/ast-utils.ts diff --git a/modules/schematics/src/schematics-core/utility/change.ts b/modules/schematics/schematics-core/utility/change.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/change.ts rename to modules/schematics/schematics-core/utility/change.ts diff --git a/modules/schematics/src/schematics-core/utility/config.ts b/modules/schematics/schematics-core/utility/config.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/config.ts rename to modules/schematics/schematics-core/utility/config.ts diff --git a/modules/schematics/src/schematics-core/utility/find-module.ts b/modules/schematics/schematics-core/utility/find-module.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/find-module.ts rename to modules/schematics/schematics-core/utility/find-module.ts diff --git a/modules/schematics/src/schematics-core/utility/ngrx-utils.ts b/modules/schematics/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/ngrx-utils.ts rename to modules/schematics/schematics-core/utility/ngrx-utils.ts diff --git a/modules/schematics/src/schematics-core/utility/project.ts b/modules/schematics/schematics-core/utility/project.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/project.ts rename to modules/schematics/schematics-core/utility/project.ts diff --git a/modules/schematics/src/schematics-core/utility/route-utils.ts b/modules/schematics/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/route-utils.ts rename to modules/schematics/schematics-core/utility/route-utils.ts diff --git a/modules/schematics/src/schematics-core/utility/strings.ts b/modules/schematics/schematics-core/utility/strings.ts similarity index 84% rename from modules/schematics/src/schematics-core/utility/strings.ts rename to modules/schematics/schematics-core/utility/strings.ts index b2832e56b7..dae37e563c 100644 --- a/modules/schematics/src/schematics-core/utility/strings.ts +++ b/modules/schematics/schematics-core/utility/strings.ts @@ -20,10 +20,6 @@ const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; decamelize('css-class-name'); // 'css-class-name' decamelize('my favorite items'); // 'my favorite items' ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. */ export function decamelize(str: string): string { return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); @@ -38,10 +34,6 @@ export function decamelize(str: string): string { dasherize('css-class-name'); // 'css-class-name' dasherize('my favorite items'); // 'my-favorite-items' ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. */ export function dasherize(str?: string): string { return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); @@ -57,10 +49,6 @@ export function dasherize(str?: string): string { camelize('my favorite items'); // 'myFavoriteItems' camelize('My Favorite Items'); // 'myFavoriteItems' ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. */ export function camelize(str: string): string { return str @@ -82,10 +70,6 @@ export function camelize(str: string): string { 'css-class-name'.classify(); // 'CssClassName' 'my favorite items'.classify(); // 'MyFavoriteItems' ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string */ export function classify(str: string): string { return str @@ -104,10 +88,6 @@ export function classify(str: string): string { 'css-class-name'.underscore(); // 'css_class_name' 'my favorite items'.underscore(); // 'my_favorite_items' ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. */ export function underscore(str: string): string { return str @@ -125,10 +105,6 @@ export function underscore(str: string): string { 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. */ export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); diff --git a/modules/schematics/src/schematics-core/utility/update.ts b/modules/schematics/schematics-core/utility/update.ts similarity index 100% rename from modules/schematics/src/schematics-core/utility/update.ts rename to modules/schematics/schematics-core/utility/update.ts diff --git a/modules/schematics/src/action/index.ts b/modules/schematics/src/action/index.ts index 9c7c2b39fa..410bc3d58a 100644 --- a/modules/schematics/src/action/index.ts +++ b/modules/schematics/src/action/index.ts @@ -14,7 +14,7 @@ import { SchematicContext, } from '@angular-devkit/schematics'; import { Schema as ActionOptions } from './schema'; -import { getProjectPath, stringUtils } from '../schematics-core'; +import { getProjectPath, stringUtils } from '@ngrx/schematics/schematics-core'; export default function(options: ActionOptions): Rule { return (host: Tree, context: SchematicContext) => { diff --git a/modules/schematics/src/container/index.ts b/modules/schematics/src/container/index.ts index 3054baf430..19ba1c43e0 100644 --- a/modules/schematics/src/container/index.ts +++ b/modules/schematics/src/container/index.ts @@ -23,7 +23,7 @@ import { InsertChange, getProjectPath, omit, -} from '../schematics-core'; +} from '@ngrx/schematics/schematics-core'; import { Schema as ContainerOptions } from './schema'; function addStateToComponent(options: ContainerOptions) { diff --git a/modules/schematics/src/effect/index.ts b/modules/schematics/src/effect/index.ts index 688fc42458..4d49154940 100644 --- a/modules/schematics/src/effect/index.ts +++ b/modules/schematics/src/effect/index.ts @@ -22,7 +22,7 @@ import { InsertChange, getProjectPath, findModuleFromOptions, -} from '../schematics-core'; +} from '@ngrx/schematics/schematics-core'; import { Schema as EffectOptions } from './schema'; function addImportToNgModule(options: EffectOptions): Rule { diff --git a/modules/schematics/src/entity/index.ts b/modules/schematics/src/entity/index.ts index b52ff98721..e6b194509d 100644 --- a/modules/schematics/src/entity/index.ts +++ b/modules/schematics/src/entity/index.ts @@ -19,7 +19,7 @@ import { addReducerImportToNgModule, getProjectPath, findModuleFromOptions, -} from '../schematics-core'; +} from '@ngrx/schematics/schematics-core'; import { Schema as EntityOptions } from './schema'; export default function(options: EntityOptions): Rule { diff --git a/modules/schematics/src/reducer/index.ts b/modules/schematics/src/reducer/index.ts index d49f6035b2..65a9de5c10 100644 --- a/modules/schematics/src/reducer/index.ts +++ b/modules/schematics/src/reducer/index.ts @@ -20,7 +20,7 @@ import { stringUtils, addReducerToState, addReducerImportToNgModule, -} from '../schematics-core'; +} from '@ngrx/schematics/schematics-core'; import { Schema as ReducerOptions } from './schema'; export default function(options: ReducerOptions): Rule { diff --git a/modules/schematics/src/schematics-core/testing/create-app-module.ts b/modules/schematics/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/schematics/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/schematics/src/schematics-core/testing/create-package.ts b/modules/schematics/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/schematics/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/schematics/src/schematics-core/testing/create-reducers.ts b/modules/schematics/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/schematics/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/schematics/src/schematics-core/testing/create-workspace.ts b/modules/schematics/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/schematics/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/schematics/src/schematics-core/testing/get-file-content.ts b/modules/schematics/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/schematics/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/schematics/src/schematics-core/testing/index.ts b/modules/schematics/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/schematics/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/schematics/src/schematics-core/testing/update.ts b/modules/schematics/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/schematics/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts b/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/schematics/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/schematics/src/schematics-core/utility/find-module_spec.ts b/modules/schematics/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/schematics/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/schematics/src/store/index.ts b/modules/schematics/src/store/index.ts index 61c1707b31..b2017a60ba 100644 --- a/modules/schematics/src/store/index.ts +++ b/modules/schematics/src/store/index.ts @@ -22,7 +22,7 @@ import { getProjectPath, findModuleFromOptions, addImportToModule, -} from '../schematics-core'; +} from '@ngrx/schematics/schematics-core'; import { Schema as StoreOptions } from './schema'; function addImportToNgModule(options: StoreOptions): Rule { diff --git a/modules/store-devtools/BUILD b/modules/store-devtools/BUILD index 6867f4c0ee..307b497be7 100644 --- a/modules/store-devtools/BUILD +++ b/modules/store-devtools/BUILD @@ -24,4 +24,8 @@ ng_package( deps = [ ":store-devtools", ], + packages = [ + "//modules/store-devtools/migrations:npm_package", + "//modules/store-devtools/schematics-core:npm_package", + ], ) diff --git a/modules/store-devtools/migrations/6_0_0/index.ts b/modules/store-devtools/migrations/6_0_0/index.ts index da9e5e6c76..b6d555bdf8 100644 --- a/modules/store-devtools/migrations/6_0_0/index.ts +++ b/modules/store-devtools/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/store-devtools/schematics-core'; export default function(): Rule { return updatePackage('store-devtools'); diff --git a/modules/store-devtools/migrations/BUILD b/modules/store-devtools/migrations/BUILD new file mode 100644 index 0000000000..073e75505f --- /dev/null +++ b/modules/store-devtools/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/store-devtools/migrations", + deps = [ + "//modules/store-devtools/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/store-devtools/schematics-core/BUILD b/modules/store-devtools/schematics-core/BUILD new file mode 100644 index 0000000000..cf9143360a --- /dev/null +++ b/modules/store-devtools/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/store-devtools/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/store-devtools/src/schematics-core/index.ts b/modules/store-devtools/schematics-core/index.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/index.ts rename to modules/store-devtools/schematics-core/index.ts diff --git a/modules/store-devtools/src/schematics-core/utility/ast-utils.ts b/modules/store-devtools/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/ast-utils.ts rename to modules/store-devtools/schematics-core/utility/ast-utils.ts diff --git a/modules/store-devtools/src/schematics-core/utility/change.ts b/modules/store-devtools/schematics-core/utility/change.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/change.ts rename to modules/store-devtools/schematics-core/utility/change.ts diff --git a/modules/store-devtools/src/schematics-core/utility/config.ts b/modules/store-devtools/schematics-core/utility/config.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/config.ts rename to modules/store-devtools/schematics-core/utility/config.ts diff --git a/modules/store-devtools/src/schematics-core/utility/find-module.ts b/modules/store-devtools/schematics-core/utility/find-module.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/find-module.ts rename to modules/store-devtools/schematics-core/utility/find-module.ts diff --git a/modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts b/modules/store-devtools/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/ngrx-utils.ts rename to modules/store-devtools/schematics-core/utility/ngrx-utils.ts diff --git a/modules/store-devtools/src/schematics-core/utility/project.ts b/modules/store-devtools/schematics-core/utility/project.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/project.ts rename to modules/store-devtools/schematics-core/utility/project.ts diff --git a/modules/store-devtools/src/schematics-core/utility/route-utils.ts b/modules/store-devtools/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/route-utils.ts rename to modules/store-devtools/schematics-core/utility/route-utils.ts diff --git a/modules/store-devtools/schematics-core/utility/strings.ts b/modules/store-devtools/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..dae37e563c --- /dev/null +++ b/modules/store-devtools/schematics-core/utility/strings.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/store-devtools/src/schematics-core/utility/update.ts b/modules/store-devtools/schematics-core/utility/update.ts similarity index 100% rename from modules/store-devtools/src/schematics-core/utility/update.ts rename to modules/store-devtools/schematics-core/utility/update.ts diff --git a/modules/store-devtools/src/schematics-core/testing/create-app-module.ts b/modules/store-devtools/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/store-devtools/src/schematics-core/testing/create-package.ts b/modules/store-devtools/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/store-devtools/src/schematics-core/testing/create-reducers.ts b/modules/store-devtools/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/store-devtools/src/schematics-core/testing/create-workspace.ts b/modules/store-devtools/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/store-devtools/src/schematics-core/testing/get-file-content.ts b/modules/store-devtools/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/store-devtools/src/schematics-core/testing/index.ts b/modules/store-devtools/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/store-devtools/src/schematics-core/testing/update.ts b/modules/store-devtools/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/store-devtools/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts b/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/store-devtools/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts b/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/store-devtools/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/store-devtools/src/schematics-core/utility/strings.ts b/modules/store-devtools/src/schematics-core/utility/strings.ts deleted file mode 100644 index b2832e56b7..0000000000 --- a/modules/store-devtools/src/schematics-core/utility/strings.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const STRING_DASHERIZE_REGEXP = /[ _]/g; -const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; -const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; -const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; -const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; - -/** - * Converts a camelized string into all lower case separated by underscores. - * - ```javascript - decamelize('innerHTML'); // 'inner_html' - decamelize('action_name'); // 'action_name' - decamelize('css-class-name'); // 'css-class-name' - decamelize('my favorite items'); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ -export function decamelize(str: string): string { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); -} - -/** - Replaces underscores, spaces, or camelCase with dashes. - - ```javascript - dasherize('innerHTML'); // 'inner-html' - dasherize('action_name'); // 'action-name' - dasherize('css-class-name'); // 'css-class-name' - dasherize('my favorite items'); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ -export function dasherize(str?: string): string { - return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); -} - -/** - Returns the lowerCamelCase form of a string. - - ```javascript - camelize('innerHTML'); // 'innerHTML' - camelize('action_name'); // 'actionName' - camelize('css-class-name'); // 'cssClassName' - camelize('my favorite items'); // 'myFavoriteItems' - camelize('My Favorite Items'); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ -export function camelize(str: string): string { - return str - .replace( - STRING_CAMELIZE_REGEXP, - (_match: string, _separator: string, chr: string) => { - return chr ? chr.toUpperCase() : ''; - } - ) - .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); -} - -/** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ -export function classify(str: string): string { - return str - .split('.') - .map(part => capitalize(camelize(part))) - .join('.'); -} - -/** - More general than decamelize. Returns the lower\_case\_and\_underscored - form of a string. - - ```javascript - 'innerHTML'.underscore(); // 'inner_html' - 'action_name'.underscore(); // 'action_name' - 'css-class-name'.underscore(); // 'css_class_name' - 'my favorite items'.underscore(); // 'my_favorite_items' - ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. - */ -export function underscore(str: string): string { - return str - .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') - .replace(STRING_UNDERSCORE_REGEXP_2, '_') - .toLowerCase(); -} - -/** - Returns the Capitalized form of a string - - ```javascript - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' - ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. - */ -export function capitalize(str: string): string { - return str.charAt(0).toUpperCase() + str.substr(1); -} - -export function group(name: string, group: string | undefined) { - return group ? `${group}/${name}` : name; -} - -export function featurePath( - group: boolean | undefined, - flat: boolean | undefined, - path: string, - name: string -) { - if (group && !flat) { - return `../../${path}/${name}/`; - } - - return group ? `../${path}/` : './'; -} diff --git a/modules/store/BUILD b/modules/store/BUILD index 4008bae0e3..61b5d5e8f1 100644 --- a/modules/store/BUILD +++ b/modules/store/BUILD @@ -21,4 +21,8 @@ ng_package( deps = [ ":store", ], + packages = [ + "//modules/store/migrations:npm_package", + "//modules/store/schematics-core:npm_package" + ] ) diff --git a/modules/store/migrations/6_0_0/index.spec.ts b/modules/store/migrations/6_0_0/index.spec.ts index 3a7f2020ec..9c869cbcf8 100644 --- a/modules/store/migrations/6_0_0/index.spec.ts +++ b/modules/store/migrations/6_0_0/index.spec.ts @@ -7,11 +7,11 @@ import * as path from 'path'; import { createPackageJson, packagePath, -} from '../../../schematics-core/testing/create-package'; +} from '../../schematics-core/testing/create-package'; import { upgradeVersion, versionPrefixes, -} from '../../../schematics-core/testing/update'; +} from '../../schematics-core/testing/update'; const collectionPath = path.join(__dirname, '../migration.json'); diff --git a/modules/store/migrations/6_0_0/index.ts b/modules/store/migrations/6_0_0/index.ts index ba2321f7a3..18abfa3174 100644 --- a/modules/store/migrations/6_0_0/index.ts +++ b/modules/store/migrations/6_0_0/index.ts @@ -1,5 +1,5 @@ import { Rule } from '@angular-devkit/schematics'; -import { updatePackage } from '../../src/schematics-core'; +import { updatePackage } from '@ngrx/store/schematics-core'; export default function(): Rule { return updatePackage('store'); diff --git a/modules/store/migrations/BUILD b/modules/store/migrations/BUILD new file mode 100644 index 0000000000..e983a84203 --- /dev/null +++ b/modules/store/migrations/BUILD @@ -0,0 +1,31 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "migrations", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/*.ts", + "**/*.spec.ts" + ], + ), + module_name = "@ngrx/store/migrations", + deps = [ + "//modules/store/schematics-core" + ] +) + +npm_package( + name = "npm_package", + srcs = [ + ":migration.json" + ], + deps = [ + ":migrations" + ] +) diff --git a/modules/store/schematics-core/BUILD b/modules/store/schematics-core/BUILD new file mode 100644 index 0000000000..89143f066e --- /dev/null +++ b/modules/store/schematics-core/BUILD @@ -0,0 +1,26 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "npm_package") + +ts_library( + name = "schematics-core", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/testing/**/*.ts", + "**/*spec.ts" + ], + ), + module_name = "@ngrx/store/schematics-core" +) + +npm_package( + name = "npm_package", + srcs = [], + deps = [ + ":schematics-core", + ], +) diff --git a/modules/store/src/schematics-core/index.ts b/modules/store/schematics-core/index.ts similarity index 100% rename from modules/store/src/schematics-core/index.ts rename to modules/store/schematics-core/index.ts diff --git a/modules/store/src/schematics-core/utility/ast-utils.ts b/modules/store/schematics-core/utility/ast-utils.ts similarity index 100% rename from modules/store/src/schematics-core/utility/ast-utils.ts rename to modules/store/schematics-core/utility/ast-utils.ts diff --git a/modules/store/src/schematics-core/utility/change.ts b/modules/store/schematics-core/utility/change.ts similarity index 100% rename from modules/store/src/schematics-core/utility/change.ts rename to modules/store/schematics-core/utility/change.ts diff --git a/modules/store/src/schematics-core/utility/config.ts b/modules/store/schematics-core/utility/config.ts similarity index 100% rename from modules/store/src/schematics-core/utility/config.ts rename to modules/store/schematics-core/utility/config.ts diff --git a/modules/store/src/schematics-core/utility/find-module.ts b/modules/store/schematics-core/utility/find-module.ts similarity index 100% rename from modules/store/src/schematics-core/utility/find-module.ts rename to modules/store/schematics-core/utility/find-module.ts diff --git a/modules/store/src/schematics-core/utility/ngrx-utils.ts b/modules/store/schematics-core/utility/ngrx-utils.ts similarity index 100% rename from modules/store/src/schematics-core/utility/ngrx-utils.ts rename to modules/store/schematics-core/utility/ngrx-utils.ts diff --git a/modules/store/src/schematics-core/utility/project.ts b/modules/store/schematics-core/utility/project.ts similarity index 100% rename from modules/store/src/schematics-core/utility/project.ts rename to modules/store/schematics-core/utility/project.ts diff --git a/modules/store/src/schematics-core/utility/route-utils.ts b/modules/store/schematics-core/utility/route-utils.ts similarity index 100% rename from modules/store/src/schematics-core/utility/route-utils.ts rename to modules/store/schematics-core/utility/route-utils.ts diff --git a/modules/store/schematics-core/utility/strings.ts b/modules/store/schematics-core/utility/strings.ts new file mode 100644 index 0000000000..dae37e563c --- /dev/null +++ b/modules/store/schematics-core/utility/strings.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const STRING_DASHERIZE_REGEXP = /[ _]/g; +const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; +const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; +const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; +const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; + +/** + * Converts a camelized string into all lower case separated by underscores. + * + ```javascript + decamelize('innerHTML'); // 'inner_html' + decamelize('action_name'); // 'action_name' + decamelize('css-class-name'); // 'css-class-name' + decamelize('my favorite items'); // 'my favorite items' + ``` + */ +export function decamelize(str: string): string { + return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); +} + +/** + Replaces underscores, spaces, or camelCase with dashes. + + ```javascript + dasherize('innerHTML'); // 'inner-html' + dasherize('action_name'); // 'action-name' + dasherize('css-class-name'); // 'css-class-name' + dasherize('my favorite items'); // 'my-favorite-items' + ``` + */ +export function dasherize(str?: string): string { + return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); +} + +/** + Returns the lowerCamelCase form of a string. + + ```javascript + camelize('innerHTML'); // 'innerHTML' + camelize('action_name'); // 'actionName' + camelize('css-class-name'); // 'cssClassName' + camelize('my favorite items'); // 'myFavoriteItems' + camelize('My Favorite Items'); // 'myFavoriteItems' + ``` + */ +export function camelize(str: string): string { + return str + .replace( + STRING_CAMELIZE_REGEXP, + (_match: string, _separator: string, chr: string) => { + return chr ? chr.toUpperCase() : ''; + } + ) + .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); +} + +/** + Returns the UpperCamelCase form of a string. + + ```javascript + 'innerHTML'.classify(); // 'InnerHTML' + 'action_name'.classify(); // 'ActionName' + 'css-class-name'.classify(); // 'CssClassName' + 'my favorite items'.classify(); // 'MyFavoriteItems' + ``` + */ +export function classify(str: string): string { + return str + .split('.') + .map(part => capitalize(camelize(part))) + .join('.'); +} + +/** + More general than decamelize. Returns the lower\_case\_and\_underscored + form of a string. + + ```javascript + 'innerHTML'.underscore(); // 'inner_html' + 'action_name'.underscore(); // 'action_name' + 'css-class-name'.underscore(); // 'css_class_name' + 'my favorite items'.underscore(); // 'my_favorite_items' + ``` + */ +export function underscore(str: string): string { + return str + .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') + .replace(STRING_UNDERSCORE_REGEXP_2, '_') + .toLowerCase(); +} + +/** + Returns the Capitalized form of a string + + ```javascript + 'innerHTML'.capitalize() // 'InnerHTML' + 'action_name'.capitalize() // 'Action_name' + 'css-class-name'.capitalize() // 'Css-class-name' + 'my favorite items'.capitalize() // 'My favorite items' + ``` + */ +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.substr(1); +} + +export function group(name: string, group: string | undefined) { + return group ? `${group}/${name}` : name; +} + +export function featurePath( + group: boolean | undefined, + flat: boolean | undefined, + path: string, + name: string +) { + if (group && !flat) { + return `../../${path}/${name}/`; + } + + return group ? `../${path}/` : './'; +} diff --git a/modules/store/src/schematics-core/utility/update.ts b/modules/store/schematics-core/utility/update.ts similarity index 100% rename from modules/store/src/schematics-core/utility/update.ts rename to modules/store/schematics-core/utility/update.ts diff --git a/modules/store/src/schematics-core/testing/create-app-module.ts b/modules/store/src/schematics-core/testing/create-app-module.ts deleted file mode 100644 index ebf3b8274c..0000000000 --- a/modules/store/src/schematics-core/testing/create-app-module.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -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'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} - -export function createAppModuleWithEffects( - tree: UnitTestTree, - path: string, - effects?: string -): UnitTestTree { - tree.create( - path || '/src/app/app.module.ts', - ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { AppComponent } from './app.component'; - import { EffectsModule } from '@ngrx/effects'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - ${effects} - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - ` - ); - - return tree; -} diff --git a/modules/store/src/schematics-core/testing/create-package.ts b/modules/store/src/schematics-core/testing/create-package.ts deleted file mode 100644 index 441c85180d..0000000000 --- a/modules/store/src/schematics-core/testing/create-package.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -export const packagePath = '/package.json'; - -export function createPackageJson( - prefix: string, - pkg: string, - tree: UnitTestTree, - version = '5.2.0', - packagePath = '/package.json' -) { - tree.create( - packagePath, - `{ - "dependencies": { - "@ngrx/${pkg}": "${prefix}5.2.0" - } - }` - ); - - return tree; -} diff --git a/modules/store/src/schematics-core/testing/create-reducers.ts b/modules/store/src/schematics-core/testing/create-reducers.ts deleted file mode 100644 index 9ee0bd4eb7..0000000000 --- a/modules/store/src/schematics-core/testing/create-reducers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; - -export function createReducers( - tree: UnitTestTree, - path?: string, - project = 'bar' -) { - tree.create( - path || `/projects/${project}/src/app/reducers/index.ts`, - ` - import { - ActionReducer, - ActionReducerMap, - createFeatureSelector, - createSelector, - MetaReducer - } from '@ngrx/store'; - import { environment } from '../../environments/environment'; - - export interface State { - - } - - export const reducers: ActionReducerMap = { - - }; - - - export const metaReducers: MetaReducer[] = !environment.production ? [] : []; - ` - ); - - return tree; -} diff --git a/modules/store/src/schematics-core/testing/create-workspace.ts b/modules/store/src/schematics-core/testing/create-workspace.ts deleted file mode 100644 index cc6e357255..0000000000 --- a/modules/store/src/schematics-core/testing/create-workspace.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - UnitTestTree, - SchematicTestRunner, -} from '@angular-devkit/schematics/testing'; - -const defaultWorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '6.0.0', -}; - -const defaultAppOptions = { - name: 'bar', - inlineStyle: false, - inlineTemplate: false, - viewEncapsulation: 'Emulated', - routing: false, - style: 'css', - skipTests: false, -}; - -const defaultModuleOptions = { - name: 'foo', - spec: true, - module: undefined, - flat: false, -}; - -export function getTestProjectPath( - workspaceOptions: any = defaultWorkspaceOptions, - appOptions: any = defaultAppOptions -) { - return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; -} - -export function createWorkspace( - schematicRunner: SchematicTestRunner, - appTree: UnitTestTree, - workspaceOptions = defaultWorkspaceOptions, - appOptions = defaultAppOptions -) { - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'workspace', - workspaceOptions - ); - appTree = schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - appOptions, - appTree - ); - - return appTree; -} diff --git a/modules/store/src/schematics-core/testing/get-file-content.ts b/modules/store/src/schematics-core/testing/get-file-content.ts deleted file mode 100644 index 44915d283e..0000000000 --- a/modules/store/src/schematics-core/testing/get-file-content.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; - -export function getFileContent(tree: Tree, path: string): string { - const fileEntry = tree.get(path); - - if (!fileEntry) { - throw new Error(`The file (${path}) does not exist.`); - } - - return fileEntry.content.toString(); -} diff --git a/modules/store/src/schematics-core/testing/index.ts b/modules/store/src/schematics-core/testing/index.ts deleted file mode 100644 index 894bca7d98..0000000000 --- a/modules/store/src/schematics-core/testing/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-app-module'; -export * from './create-reducers'; -export * from './create-workspace'; -export * from './get-file-content'; diff --git a/modules/store/src/schematics-core/testing/update.ts b/modules/store/src/schematics-core/testing/update.ts deleted file mode 100644 index d451bf0513..0000000000 --- a/modules/store/src/schematics-core/testing/update.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const upgradeVersion = '6.0.0-beta.2'; -export const versionPrefixes = ['~', '^', '']; diff --git a/modules/store/src/schematics-core/utility/ast-utils_spec.ts b/modules/store/src/schematics-core/utility/ast-utils_spec.ts deleted file mode 100644 index 4290558148..0000000000 --- a/modules/store/src/schematics-core/utility/ast-utils_spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { tags } from '@angular-devkit/core'; -import { VirtualTree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; -import { Change, InsertChange } from './change'; -import { getFileContent } from '../testing'; -import { addExportToModule } from './ast-utils'; - -function getTsSource(path: string, content: string): ts.SourceFile { - return ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); -} - -function applyChanges( - path: string, - content: string, - changes: Change[] -): string { - const tree = new VirtualTree(); - tree.create(path, content); - const exportRecorder = tree.beginUpdate(path); - for (const change of changes) { - if (change instanceof InsertChange) { - exportRecorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(exportRecorder); - - return getFileContent(tree, path); -} - -describe('ast utils', () => { - let modulePath: string; - let moduleContent: string; - beforeEach(() => { - modulePath = '/src/app/app.module.ts'; - moduleContent = ` - 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 { } - `; - }); - - it('should add export to module', () => { - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); - - it('should add export to module if not indented', () => { - moduleContent = tags.stripIndent`${moduleContent}`; - const source = getTsSource(modulePath, moduleContent); - const changes = addExportToModule( - source, - modulePath, - 'FooComponent', - './foo.component' - ); - const output = applyChanges(modulePath, moduleContent, changes); - expect(output).toMatch(/import { FooComponent } from '.\/foo.component';/); - expect(output).toMatch(/exports: \[FooComponent\]/); - }); -}); diff --git a/modules/store/src/schematics-core/utility/find-module_spec.ts b/modules/store/src/schematics-core/utility/find-module_spec.ts deleted file mode 100644 index ecdfe1a3f2..0000000000 --- a/modules/store/src/schematics-core/utility/find-module_spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { EmptyTree, Tree } from '@angular-devkit/schematics'; -import { findModule } from './find-module'; - -describe('find-module', () => { - describe('findModule', () => { - let host: Tree; - const modulePath = '/foo/src/app/app.module.ts'; - beforeEach(() => { - host = new EmptyTree(); - host.create(modulePath, 'app module'); - }); - - it('should find a module', () => { - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should not find a module in another sub dir', () => { - host.create('/foo/src/app/buzz/buzz.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should ignore routing modules', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo/src/app/bar'); - expect(foundModule).toEqual(modulePath); - }); - - it('should work with weird paths', () => { - host.create('/foo/src/app/app-routing.module.ts', 'app module'); - const foundModule = findModule(host, 'foo//src//app/bar/'); - expect(foundModule).toEqual(modulePath); - }); - - it('should throw if no modules found', () => { - host.create('/foo/src/app/oops.module.ts', 'app module'); - try { - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/More than one module matches/); - } - }); - - it('should throw if two modules found', () => { - try { - host = new EmptyTree(); - findModule(host, 'foo/src/app/bar'); - throw new Error('Succeeded, should have failed'); - } catch (err) { - expect(err.message).toMatch(/Could not find an NgModule/); - } - }); - }); -}); diff --git a/modules/store/src/schematics-core/utility/strings.ts b/modules/store/src/schematics-core/utility/strings.ts deleted file mode 100644 index b2832e56b7..0000000000 --- a/modules/store/src/schematics-core/utility/strings.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -const STRING_DASHERIZE_REGEXP = /[ _]/g; -const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; -const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; -const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g; -const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g; - -/** - * Converts a camelized string into all lower case separated by underscores. - * - ```javascript - decamelize('innerHTML'); // 'inner_html' - decamelize('action_name'); // 'action_name' - decamelize('css-class-name'); // 'css-class-name' - decamelize('my favorite items'); // 'my favorite items' - ``` - - @method decamelize - @param {String} str The string to decamelize. - @return {String} the decamelized string. - */ -export function decamelize(str: string): string { - return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); -} - -/** - Replaces underscores, spaces, or camelCase with dashes. - - ```javascript - dasherize('innerHTML'); // 'inner-html' - dasherize('action_name'); // 'action-name' - dasherize('css-class-name'); // 'css-class-name' - dasherize('my favorite items'); // 'my-favorite-items' - ``` - - @method dasherize - @param {String} str The string to dasherize. - @return {String} the dasherized string. - */ -export function dasherize(str?: string): string { - return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-'); -} - -/** - Returns the lowerCamelCase form of a string. - - ```javascript - camelize('innerHTML'); // 'innerHTML' - camelize('action_name'); // 'actionName' - camelize('css-class-name'); // 'cssClassName' - camelize('my favorite items'); // 'myFavoriteItems' - camelize('My Favorite Items'); // 'myFavoriteItems' - ``` - - @method camelize - @param {String} str The string to camelize. - @return {String} the camelized string. - */ -export function camelize(str: string): string { - return str - .replace( - STRING_CAMELIZE_REGEXP, - (_match: string, _separator: string, chr: string) => { - return chr ? chr.toUpperCase() : ''; - } - ) - .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); -} - -/** - Returns the UpperCamelCase form of a string. - - ```javascript - 'innerHTML'.classify(); // 'InnerHTML' - 'action_name'.classify(); // 'ActionName' - 'css-class-name'.classify(); // 'CssClassName' - 'my favorite items'.classify(); // 'MyFavoriteItems' - ``` - - @method classify - @param {String} str the string to classify - @return {String} the classified string - */ -export function classify(str: string): string { - return str - .split('.') - .map(part => capitalize(camelize(part))) - .join('.'); -} - -/** - More general than decamelize. Returns the lower\_case\_and\_underscored - form of a string. - - ```javascript - 'innerHTML'.underscore(); // 'inner_html' - 'action_name'.underscore(); // 'action_name' - 'css-class-name'.underscore(); // 'css_class_name' - 'my favorite items'.underscore(); // 'my_favorite_items' - ``` - - @method underscore - @param {String} str The string to underscore. - @return {String} the underscored string. - */ -export function underscore(str: string): string { - return str - .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2') - .replace(STRING_UNDERSCORE_REGEXP_2, '_') - .toLowerCase(); -} - -/** - Returns the Capitalized form of a string - - ```javascript - 'innerHTML'.capitalize() // 'InnerHTML' - 'action_name'.capitalize() // 'Action_name' - 'css-class-name'.capitalize() // 'Css-class-name' - 'my favorite items'.capitalize() // 'My favorite items' - ``` - - @method capitalize - @param {String} str The string to capitalize. - @return {String} The capitalized string. - */ -export function capitalize(str: string): string { - return str.charAt(0).toUpperCase() + str.substr(1); -} - -export function group(name: string, group: string | undefined) { - return group ? `${group}/${name}` : name; -} - -export function featurePath( - group: boolean | undefined, - flat: boolean | undefined, - path: string, - name: string -) { - if (group && !flat) { - return `../../${path}/${name}/`; - } - - return group ? `../${path}/` : './'; -} diff --git a/package.json b/package.json index 182c41b509..731849ab50 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "precommit": "lint-staged", "bootstrap": "lerna bootstrap", + "prebuild": "yarn copy:schematics", "build": "bazel build ...", "deploy:builds": "ts-node ./build/deploy-build.ts", "test:unit": "node ./tests.js", @@ -26,7 +27,7 @@ "codegen": "ts-node modules/codegen/src/index.ts", "prebuildifier": "bazel build --noshow_progress @com_github_bazelbuild_buildtools//buildifier", "buildifier": "find . -type f \\( -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/buildifier", - "copy:schematics": "ncp modules/schematics-core/ modules/schematics/src/schematics-core/" + "copy:schematics": "ts-node ./build/copy-schematics-core.ts" }, "engines": { "node": ">=8.9.0", @@ -94,6 +95,7 @@ "@types/jasminewd2": "^2.0.2", "@types/jest": "^20.0.2", "@types/lodash": "^4.14.80", + "@types/ncp": "^2.0.1", "@types/node": "^7.0.5", "@types/ora": "^1.3.1", "@types/rimraf": "^0.0.28", diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 1272a54cb5..fd41346751 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -2,7 +2,7 @@ load("@build_bazel_rules_typescript//:defs.bzl", _ts_library="ts_library") load("@angular//:index.bzl", _ng_module="ng_module", _ng_package="ng_package") load("@build_bazel_rules_nodejs//:defs.bzl", - _jasmine_node_test="jasmine_node_test") + _jasmine_node_test="jasmine_node_test", _npm_package="npm_package") DEFAULT_TSCONFIG = "//:tsconfig.json" NG_VERSION = "^6.0.0" @@ -76,3 +76,9 @@ def ng_package(name, readme_md=None, license_banner=None, globals={}, **kwargs): globals=dict(globals, **NGRX_GLOBALS), replacements=PKG_GROUP_REPLACEMENTS, **kwargs) + +def npm_package(name, replacements = {}, **kwargs): + _npm_package( + name = name, + replacements = dict(replacements, **PKG_GROUP_REPLACEMENTS), + **kwargs) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e94d91d5e2..70b47b2095 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,11 +16,23 @@ "strict": true, "paths": { "@ngrx/effects": ["./modules/effects"], + "@ngrx/effects/schematics-core": ["./modules/effects/schematics-core"], "@ngrx/store": ["./modules/store"], + "@ngrx/store/schematics-core": ["./modules/store/schematics-core"], "@ngrx/store-devtools": ["./modules/store-devtools"], + "@ngrx/store-devtools/schematics-core": [ + "./modules/store-devtools/schematics-core" + ], "@ngrx/router-store": ["./modules/router-store"], + "@ngrx/router-store/schematics-core": [ + "./modules/router-store/schematics-core" + ], "@ngrx/entity": ["./modules/entity"], - "@ngrx/schematics": ["./modules/schematics"] + "@ngrx/entity/schematics-core": ["./modules/entity/schematics-core"], + "@ngrx/schematics": ["./modules/schematics"], + "@ngrx/schematics/schematics-core": [ + "./modules/schematics/schematics-core" + ] } }, "exclude": [ diff --git a/yarn.lock b/yarn.lock index 0c270cbd72..b4eedd0df9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -279,6 +279,12 @@ version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" +"@types/ncp@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/ncp/-/ncp-2.0.1.tgz#749432511f6ad747d04e98837b18cca9045567fb" + dependencies: + "@types/node" "*" + "@types/node@*": version "10.0.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.0.6.tgz#c0bce8e539bf34c1b850c13ff46bead2fecc2e58" From 56efc9157e593b72975cd2bca945dd9a45007d34 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 11 May 2018 12:35:41 -0500 Subject: [PATCH 3/4] fixup! feat: Add ng update support to ngrx packages --- modules/effects/BUILD | 8 ++++---- modules/effects/migrations/BUILD | 12 ++++++------ modules/effects/schematics-core/BUILD | 4 ++-- modules/entity/BUILD | 6 +++--- modules/entity/migrations/BUILD | 12 ++++++------ modules/entity/schematics-core/BUILD | 4 ++-- modules/router-store/BUILD | 6 +++--- modules/router-store/migrations/BUILD | 12 ++++++------ modules/router-store/schematics-core/BUILD | 4 ++-- modules/schematics/BUILD | 10 +++++----- modules/schematics/migrations/BUILD | 12 ++++++------ modules/schematics/schematics-core/BUILD | 4 ++-- modules/store-devtools/BUILD | 6 +++--- modules/store-devtools/migrations/BUILD | 12 ++++++------ modules/store-devtools/schematics-core/BUILD | 4 ++-- modules/store/BUILD | 8 ++++---- modules/store/migrations/BUILD | 12 ++++++------ modules/store/schematics-core/BUILD | 4 ++-- 18 files changed, 70 insertions(+), 70 deletions(-) diff --git a/modules/effects/BUILD b/modules/effects/BUILD index 3684c5f53a..f604dd153d 100644 --- a/modules/effects/BUILD +++ b/modules/effects/BUILD @@ -23,12 +23,12 @@ ng_package( "//modules/effects/testing:package.json", ], entry_point = "modules/effects/index.js", - deps = [ - ":effects", - "//modules/effects/testing", - ], packages = [ "//modules/effects/migrations:npm_package", "//modules/effects/schematics-core:npm_package", ], + deps = [ + ":effects", + "//modules/effects/testing", + ], ) diff --git a/modules/effects/migrations/BUILD b/modules/effects/migrations/BUILD index 3c392f5733..5784a4c887 100644 --- a/modules/effects/migrations/BUILD +++ b/modules/effects/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/effects/migrations", deps = [ - "//modules/effects/schematics-core" - ] + "//modules/effects/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/effects/schematics-core/BUILD b/modules/effects/schematics-core/BUILD index a3ad259df2..14e42bfc19 100644 --- a/modules/effects/schematics-core/BUILD +++ b/modules/effects/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/effects/schematics-core" + module_name = "@ngrx/effects/schematics-core", ) npm_package( diff --git a/modules/entity/BUILD b/modules/entity/BUILD index ac72887e63..fe8bfcf8cc 100644 --- a/modules/entity/BUILD +++ b/modules/entity/BUILD @@ -20,11 +20,11 @@ ng_package( "package.json", ], entry_point = "modules/entity/index.js", - deps = [ - ":entity", - ], packages = [ "//modules/entity/migrations:npm_package", "//modules/entity/schematics-core:npm_package", ], + deps = [ + ":entity", + ], ) diff --git a/modules/entity/migrations/BUILD b/modules/entity/migrations/BUILD index eecfe74d4c..9107840bbe 100644 --- a/modules/entity/migrations/BUILD +++ b/modules/entity/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/entity/migrations", deps = [ - "//modules/entity/schematics-core" - ] + "//modules/entity/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/entity/schematics-core/BUILD b/modules/entity/schematics-core/BUILD index f887538800..5b5879c822 100644 --- a/modules/entity/schematics-core/BUILD +++ b/modules/entity/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/entity/schematics-core" + module_name = "@ngrx/entity/schematics-core", ) npm_package( diff --git a/modules/router-store/BUILD b/modules/router-store/BUILD index bd4c2db856..5d7f6cb077 100644 --- a/modules/router-store/BUILD +++ b/modules/router-store/BUILD @@ -21,11 +21,11 @@ ng_package( "package.json", ], entry_point = "modules/router-store/index.js", - deps = [ - ":router-store", - ], packages = [ "//modules/router-store/migrations:npm_package", "//modules/router-store/schematics-core:npm_package", ], + deps = [ + ":router-store", + ], ) diff --git a/modules/router-store/migrations/BUILD b/modules/router-store/migrations/BUILD index 3cc1026d5d..b6363b7ad0 100644 --- a/modules/router-store/migrations/BUILD +++ b/modules/router-store/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/router-store/migrations", deps = [ - "//modules/router-store/schematics-core" - ] + "//modules/router-store/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/router-store/schematics-core/BUILD b/modules/router-store/schematics-core/BUILD index ceab380ddc..6992b3e8c6 100644 --- a/modules/router-store/schematics-core/BUILD +++ b/modules/router-store/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/router-store/schematics-core" + module_name = "@ngrx/router-store/schematics-core", ) npm_package( diff --git a/modules/schematics/BUILD b/modules/schematics/BUILD index c30b36a177..ff22b46cf8 100644 --- a/modules/schematics/BUILD +++ b/modules/schematics/BUILD @@ -17,8 +17,8 @@ ts_library( module_name = "@ngrx/schematics", tsconfig = ":tsconfig-build.json", deps = [ - "//modules/schematics/schematics-core" - ] + "//modules/schematics/schematics-core", + ], ) npm_package( @@ -32,11 +32,11 @@ npm_package( "**/schema.json", "**/migration.json", ]), - deps = [ - ":schematics", - ], packages = [ "//modules/schematics/migrations:npm_package", "//modules/schematics/schematics-core:npm_package", ], + deps = [ + ":schematics", + ], ) diff --git a/modules/schematics/migrations/BUILD b/modules/schematics/migrations/BUILD index 99b9cff104..b1972513c1 100644 --- a/modules/schematics/migrations/BUILD +++ b/modules/schematics/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/schematics/migrations", deps = [ - "//modules/schematics/schematics-core" - ] + "//modules/schematics/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/schematics/schematics-core/BUILD b/modules/schematics/schematics-core/BUILD index 97bdee518a..37cc4c8db3 100644 --- a/modules/schematics/schematics-core/BUILD +++ b/modules/schematics/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/schematics/schematics-core" + module_name = "@ngrx/schematics/schematics-core", ) npm_package( diff --git a/modules/store-devtools/BUILD b/modules/store-devtools/BUILD index 307b497be7..f1cb949c1a 100644 --- a/modules/store-devtools/BUILD +++ b/modules/store-devtools/BUILD @@ -21,11 +21,11 @@ ng_package( "package.json", ], entry_point = "modules/store-devtools/index.js", - deps = [ - ":store-devtools", - ], packages = [ "//modules/store-devtools/migrations:npm_package", "//modules/store-devtools/schematics-core:npm_package", ], + deps = [ + ":store-devtools", + ], ) diff --git a/modules/store-devtools/migrations/BUILD b/modules/store-devtools/migrations/BUILD index 073e75505f..be0d9e2ec0 100644 --- a/modules/store-devtools/migrations/BUILD +++ b/modules/store-devtools/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/store-devtools/migrations", deps = [ - "//modules/store-devtools/schematics-core" - ] + "//modules/store-devtools/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/store-devtools/schematics-core/BUILD b/modules/store-devtools/schematics-core/BUILD index cf9143360a..a1328889fc 100644 --- a/modules/store-devtools/schematics-core/BUILD +++ b/modules/store-devtools/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/store-devtools/schematics-core" + module_name = "@ngrx/store-devtools/schematics-core", ) npm_package( diff --git a/modules/store/BUILD b/modules/store/BUILD index 61b5d5e8f1..7590508018 100644 --- a/modules/store/BUILD +++ b/modules/store/BUILD @@ -18,11 +18,11 @@ ng_package( "package.json", ], entry_point = "modules/store/index.js", + packages = [ + "//modules/store/migrations:npm_package", + "//modules/store/schematics-core:npm_package", + ], deps = [ ":store", ], - packages = [ - "//modules/store/migrations:npm_package", - "//modules/store/schematics-core:npm_package" - ] ) diff --git a/modules/store/migrations/BUILD b/modules/store/migrations/BUILD index e983a84203..ddda06b952 100644 --- a/modules/store/migrations/BUILD +++ b/modules/store/migrations/BUILD @@ -11,21 +11,21 @@ ts_library( ], exclude = [ "**/testing/*.ts", - "**/*.spec.ts" + "**/*.spec.ts", ], ), module_name = "@ngrx/store/migrations", deps = [ - "//modules/store/schematics-core" - ] + "//modules/store/schematics-core", + ], ) npm_package( name = "npm_package", srcs = [ - ":migration.json" + ":migration.json", ], deps = [ - ":migrations" - ] + ":migrations", + ], ) diff --git a/modules/store/schematics-core/BUILD b/modules/store/schematics-core/BUILD index 89143f066e..4fa2816de9 100644 --- a/modules/store/schematics-core/BUILD +++ b/modules/store/schematics-core/BUILD @@ -11,10 +11,10 @@ ts_library( ], exclude = [ "**/testing/**/*.ts", - "**/*spec.ts" + "**/*spec.ts", ], ), - module_name = "@ngrx/store/schematics-core" + module_name = "@ngrx/store/schematics-core", ) npm_package( From 9bc0f4ce9b43ef5b2ee5131717e2b41a311e011f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 11 May 2018 12:41:39 -0500 Subject: [PATCH 4/4] Fix test path --- modules/store/migrations/6_0_0/index.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/store/migrations/6_0_0/index.spec.ts b/modules/store/migrations/6_0_0/index.spec.ts index 9c869cbcf8..3a7f2020ec 100644 --- a/modules/store/migrations/6_0_0/index.spec.ts +++ b/modules/store/migrations/6_0_0/index.spec.ts @@ -7,11 +7,11 @@ import * as path from 'path'; import { createPackageJson, packagePath, -} from '../../schematics-core/testing/create-package'; +} from '../../../schematics-core/testing/create-package'; import { upgradeVersion, versionPrefixes, -} from '../../schematics-core/testing/update'; +} from '../../../schematics-core/testing/update'; const collectionPath = path.join(__dirname, '../migration.json');