├── .gitignore ├── .npmignore ├── .prettierignore ├── README.md ├── docs ├── webstorm-1.png └── webstorm-2.png ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── ast │ ├── add-default-value-to-parent-state-functions │ │ ├── add-default-value-to-parent-state-functions.spec.ts │ │ ├── add-default-value-to-parent-state-functions.ts │ │ ├── app-state.functions.empty.txt │ │ └── app-state.functions.existing.txt │ ├── add-effects-to-module │ │ ├── add-effects-to-module.spec.ts │ │ ├── add-effects-to-module.ts │ │ ├── app.module.has-empty-effects-module.txt │ │ ├── app.module.has-existing-effects-module.txt │ │ ├── app.module.no-effects-module.txt │ │ └── app.module.no-imports.txt │ ├── add-reducer-to-parent-reducer │ │ ├── add-reducer-to-parent-reducer.spec.ts │ │ ├── add-reducer-to-parent-reducer.ts │ │ ├── app.reducer.txt │ │ ├── feature.reducer.has-child-states.txt │ │ └── feature.reducer.no-child-states.txt │ ├── add-route-to-routing-module │ │ ├── add-route-to-routing-module.spec.ts │ │ ├── add-route-to-routing-module.ts │ │ ├── app-routing.module.empty-routes.txt │ │ └── app-routing.module.existing-routes.txt │ ├── add-state-member-to-parent-state-interface │ │ ├── add-state-member-to-parent-state-interface.spec.ts │ │ ├── add-state-member-to-parent-state-interface.ts │ │ ├── app-state.interface.txt │ │ └── feature-state.interface.txt │ ├── ast-helpers.ts │ ├── ast-wrappers.ts │ ├── rework-app-reducer │ │ ├── app.reducer.txt │ │ ├── rework-app-reducer.spec.ts │ │ └── rework-app-reducer.ts │ └── source-file-modification.interface.ts ├── public_api.ts ├── rules │ ├── add-npm-dependencies.rule.ts │ ├── add-npm-scripts.rule.ts │ ├── delete-files.rule.ts │ ├── modify-source-file.rule.ts │ ├── process-templates.rule.ts │ ├── run-npm-install.rule.ts │ ├── run-prettier.rule.ts │ ├── run-tslint-fix.rule.ts │ ├── tree-helpers.ts │ └── update-barrel-file.rule.ts ├── schematics │ ├── collection.json │ ├── component │ │ ├── files │ │ │ └── __name@dasherize__ │ │ │ │ ├── __name@dasherize__.component.html │ │ │ │ ├── __name@dasherize__.component.scss │ │ │ │ └── __name@dasherize__.component.ts │ │ ├── index.ts │ │ └── schema.json │ ├── feature │ │ ├── files │ │ │ ├── __name@dasherize__-routing.module.ts │ │ │ └── __name@dasherize__.module.ts │ │ ├── index.ts │ │ └── schema.json │ ├── index.ts │ ├── initialize │ │ ├── files │ │ │ └── app.store.ts │ │ ├── index.ts │ │ └── schema.json │ ├── module │ │ ├── files │ │ │ └── __name@dasherize__.module.ts │ │ ├── index.ts │ │ └── schema.json │ ├── ngrx-actions │ │ ├── files │ │ │ └── __name@dasherize__.actions.ts │ │ ├── index.ts │ │ └── schema.json │ ├── ngrx-effects │ │ ├── files │ │ │ └── __name@dasherize__.effects.ts │ │ ├── index.ts │ │ └── schema.json │ ├── ngrx-reducer │ │ ├── files │ │ │ └── __name@dasherize__.reducer.ts │ │ ├── index.ts │ │ └── schema.json │ ├── ngrx-store │ │ ├── files │ │ │ └── __name@dasherize__.store.ts │ │ ├── index.ts │ │ └── schema.json │ ├── ngrx │ │ ├── index.ts │ │ └── schema.json │ ├── service │ │ ├── files │ │ │ └── __name@dasherize__.service.ts │ │ ├── index.ts │ │ └── schema.json │ ├── tslint-and-prettier │ │ ├── files │ │ │ ├── prettier.config.js │ │ │ └── tslint.json │ │ ├── index.ts │ │ └── schema.json │ └── type │ │ ├── files │ │ └── __name@dasherize__ │ │ │ ├── __name@dasherize__.functions.spec.ts │ │ │ ├── __name@dasherize__.functions.ts │ │ │ └── __name@dasherize__.interface.ts │ │ ├── index.ts │ │ ├── schema.json │ │ └── type-schema-options.interface.ts └── types │ ├── folders │ └── folders.enum.ts │ ├── npm-script │ └── npm-script.interface.ts │ ├── path-options │ ├── path-options.functions.ts │ └── path-options.interface.ts │ ├── project-schema-options │ ├── project-schema-options.functions.ts │ └── project-schema-options.interface.ts │ └── schema-options │ ├── schema-options.functions.ts │ └── schema-options.interface.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Outputs 2 | dist/ 3 | 4 | # IDEs 5 | .idea/ 6 | jsconfig.json 7 | .vscode/ 8 | 9 | # Misc 10 | node_modules/ 11 | npm-debug.log* 12 | yarn-error.log* 13 | 14 | # Mac OSX Finder files. 15 | **/.DS_Store 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignores TypeScript files, but keeps definitions. 2 | *.ts 3 | !*.d.ts 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/files/**/*.ts 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Schematics for Productivity and Consistency 2 | 3 | Developing large-scale web-applications is *hard*, and without a lot of 4 | experience building large Angular applications, it's very easy to make 5 | an architectural or structural mistake that could cost your team a lot 6 | of money and time to fix down the road once that mistake is realized. 7 | 8 | The schematics in this library will help your team develop Angular 9 | applications that are consistent and have an architecture/folder 10 | structure that will scale for extremely large projects right from the 11 | get-go. 12 | 13 | The output of these schematics is based on many years of experience 14 | building large-scale projects in Angular, and have been used in 15 | applications that contain several hundred-thousand lines of code. Thus, 16 | the opinions and ideas behind these schematics have been battle-tested 17 | and proved to prevent and solve scalability problems many people run 18 | into when building large-scale Angular projects. 19 | 20 | ## Installation 21 | 22 | To add these schematics to your angular-cli project, type the 23 | following in the console at the root of your project: 24 | 25 | ```bash 26 | npm install @egervari/schematics-angular --save-dev 27 | ``` 28 | 29 | To run schematics from your console, also add the following global 30 | npm library: 31 | 32 | ```bash 33 | npm install -g @angular-devkit/schematics-cli 34 | ``` 35 | 36 | ### Using Schematics Via Console 37 | 38 | #### Adding prettier and overwriting TSLint and Prettier rules 39 | 40 | To add prettier (if you currently don't have it installed) and use 41 | Rangle's preferred starting point for TSLint and Prettier rules, run 42 | the following schematic from your console: 43 | 44 | ```bash 45 | schematics @egervari/schematics-angular:tslint-and-prettier 46 | ``` 47 | 48 | Running this schematic will also create a bunch of scripts to 49 | conveniently check and fix your project so you don't have to add them 50 | yourself. 51 | 52 | You should see the following updates to your project: 53 | 54 | ```bash 55 | Added "prettier" into devDependencies 56 | Added "tslint-config-prettier" into devDependencies 57 | CREATE /prettier.config.js (173 bytes) 58 | UPDATE /tslint.json (869 bytes) 59 | UPDATE /package.json (2552 bytes) 60 | ``` 61 | 62 | #### Creating Features 63 | 64 | To create a feature module containing components, routes, services, 65 | types and isolated ngrx infrastructure, you must specify the feature's 66 | `name`, `path` and optionally, you can specify your project's component 67 | `prefix` for its selector, but the schematic can often figure this out 68 | from your Angular project workspace. 69 | 70 | ```bash 71 | schematics @egervari/schematics-angular:feature --name=my-feature --path=src/app --prefix=rng 72 | ``` 73 | 74 | If your project currently does not have a `/features` folder, the 75 | schematic will create one for you. 76 | 77 | You should see the following files be created/updated when running this 78 | schematic: 79 | 80 | ```bash 81 | CREATE /src/app/features/my-feature/my-feature-routing.module.ts (370 bytes) 82 | CREATE /src/app/features/my-feature/my-feature.module.ts (676 bytes) 83 | CREATE /src/app/features/my-feature/components/my-feature/my-feature.component.html (0 bytes) 84 | CREATE /src/app/features/my-feature/components/my-feature/my-feature.component.scss (12 bytes) 85 | CREATE /src/app/features/my-feature/components/my-feature/my-feature.component.ts (224 bytes) 86 | CREATE /src/app/features/my-feature/components/index.ts (52 bytes) 87 | CREATE /src/app/features/my-feature/services/my-feature.service.ts (200 bytes) 88 | CREATE /src/app/features/my-feature/store/my-feature.actions.ts (239 bytes) 89 | CREATE /src/app/features/my-feature/store/my-feature.effects.ts (453 bytes) 90 | CREATE /src/app/features/my-feature/store/my-feature.reducer.ts (560 bytes) 91 | CREATE /src/app/features/my-feature/store/my-feature.store.ts (305 bytes) 92 | CREATE /src/app/features/my-feature/types/my-feature-state/my-feature-state.functions.spec.ts (419 bytes) 93 | CREATE /src/app/features/my-feature/types/my-feature-state/my-feature-state.functions.ts (158 bytes) 94 | CREATE /src/app/features/my-feature/types/my-feature-state/my-feature-state.interface.ts (56 bytes) 95 | UPDATE /src/app/store/app.reducer.ts (543 bytes) 96 | UPDATE /src/app/types/app-state/app-state.interface.ts (175 bytes) 97 | UPDATE /src/app/types/app-state/app-state.functions.ts (273 bytes) 98 | UPDATE /src/app/app-routing.module.ts (355 bytes) 99 | ``` 100 | 101 | #### Creating Modules 102 | 103 | Sometimes you wish to create modules outside of your routes. They are 104 | not really features, but reusable pieces of code the rest of the app 105 | can share. To create such a module, run the following schematic: 106 | 107 | ```bash 108 | schematics @egervari/schematics-angular:module --name=my-module --path=src/app 109 | ``` 110 | 111 | If your project currently does not have a `/modules` folder, the 112 | schematic will create one for you. 113 | 114 | You should see the following files be created/updated when running this 115 | schematic: 116 | 117 | ```bash 118 | CREATE /src/app/modules/my-module/my-module.module.ts (559 bytes) 119 | CREATE /src/app/modules/my-module/services/my-module.service.ts (199 bytes) 120 | CREATE /src/app/modules/my-module/store/my-module.actions.ts (236 bytes) 121 | CREATE /src/app/modules/my-module/store/my-module.effects.ts (448 bytes) 122 | CREATE /src/app/modules/my-module/store/my-module.reducer.ts (547 bytes) 123 | CREATE /src/app/modules/my-module/store/my-module.store.ts (303 bytes) 124 | CREATE /src/app/modules/my-module/types/my-module-state/my-module-state.functions.spec.ts (410 bytes) 125 | CREATE /src/app/modules/my-module/types/my-module-state/my-module-state.functions.ts (154 bytes) 126 | CREATE /src/app/modules/my-module/types/my-module-state/my-module-state.interface.ts (55 bytes) 127 | UPDATE /src/app/store/app.reducer.ts (660 bytes) 128 | UPDATE /src/app/types/app-state/app-state.interface.ts (314 bytes) 129 | UPDATE /src/app/types/app-state/app-state.functions.ts (425 bytes) 130 | UPDATE /src/app/app.module.ts (768 bytes) 131 | ``` 132 | 133 | #### Creating Components 134 | 135 | Creating components is equally easy - make sure to point the `path` 136 | to the module/feature folder you wish the component to be located: 137 | 138 | ```bash 139 | schematics @egervari/schematics-angular:component --name=my-form --path=src/app 140 | ``` 141 | 142 | If your path currently does not have a `/components` folder, the 143 | schematic will create one for you. 144 | 145 | You should see the following files be created/updated when running this 146 | schematic: 147 | 148 | ```bash 149 | CREATE /src/app/components/my-form/my-form.component.html (0 bytes) 150 | CREATE /src/app/components/my-form/my-form.component.scss (12 bytes) 151 | CREATE /src/app/components/my-form/my-form.component.ts (214 bytes) 152 | UPDATE /src/app/components/index.ts (192 bytes) 153 | UPDATE /src/app/app.module.ts (2484 bytes) 154 | ``` 155 | 156 | #### Creating Services 157 | 158 | Creates a service and updates the containing module file. 159 | 160 | ```bash 161 | schematics @egervari/schematics-angular:service --name=my-api --path=src/app 162 | ``` 163 | 164 | If your path currently does not have a `/services` folder, the 165 | schematic will create one for you. 166 | 167 | You should see the following files be created/updated when running this 168 | schematic: 169 | 170 | ```bash 171 | CREATE /src/app/services/my-api.service.ts (200 bytes) 172 | UPDATE /src/app/app.module.ts (2561 bytes) 173 | ``` 174 | 175 | #### Creating Types 176 | 177 | Creates a paired interface and functions file for a given model. 178 | 179 | ```bash 180 | schematics @egervari/schematics-angular:type --name=user-profile --path=src/app 181 | ``` 182 | 183 | If your path currently does not have a `/types` folder, the 184 | schematic will create one for you. 185 | 186 | You should see the following files be created when running this 187 | schematic: 188 | 189 | ```bash 190 | CREATE /src/app/types/user-profile/user-profile.functions.spec.ts (390 bytes) 191 | CREATE /src/app/types/user-profile/user-profile.functions.ts (146 bytes) 192 | CREATE /src/app/types/user-profile/user-profile.interface.ts (55 bytes) 193 | ``` 194 | 195 | #### Creating a new project 196 | 197 | The Angular CLI and the default @ngrx schematics do not create folders 198 | and files that help developers scale over the long term. Thus, we also 199 | provide a schematic to make some adjustments to a newly created Angular 200 | CLI project to help you get started on the right foot and to support the 201 | other schematics. 202 | 203 | To create a new Angular CLI Project, execute the following commands 204 | and you're good to go: 205 | 206 | ```bash 207 | npm install @angular/cli -g 208 | 209 | ng new my-project 210 | ``` 211 | 212 | When asked: 213 | 214 | * Say **(Y)es** to add Angular Routing 215 | * Select **SCSS** for your stylesheet format 216 | 217 | ```bash 218 | cd my-project 219 | 220 | ng add @ngrx/store 221 | ng add @ngrx/effects 222 | 223 | npm install @egervari/schematics-angular --save-dev 224 | ``` 225 | 226 | And then to correct the default structure, run the `initialize` 227 | schematic with no parameters: 228 | 229 | ```bash 230 | schematics @egervari/schematics-angular:initialize 231 | ``` 232 | 233 | You should see the following changes: 234 | 235 | ```bash 236 | Added "prettier" into devDependencies 237 | Added "tslint-config-prettier" into devDependencies 238 | DELETE /src/app/app.component.html 239 | DELETE /src/app/app.component.scss 240 | DELETE /src/app/app.component.ts 241 | DELETE /src/app/reducers/index.ts 242 | DELETE /src/app/app.effects.ts 243 | DELETE /src/app/app.component.spec.ts 244 | DELETE /src/app/app.effects.spec.ts 245 | CREATE /prettier.config.js (163 bytes) 246 | CREATE /src/app/types/app-state/app-state.functions.ts (118 bytes) 247 | CREATE /src/app/types/app-state/app-state.interface.ts (33 bytes) 248 | CREATE /src/app/components/index.ts (35 bytes) 249 | CREATE /src/app/components/app/app.component.html (1173 bytes) 250 | CREATE /src/app/components/app/app.component.scss (0 bytes) 251 | CREATE /src/app/components/app/app.component.ts (215 bytes) 252 | CREATE /src/app/store/app.reducer.ts (403 bytes) 253 | CREATE /src/app/store/app.effects.ts (180 bytes) 254 | CREATE /src/app/store/app.store.ts (385 bytes) 255 | UPDATE /tslint.json (826 bytes) 256 | UPDATE /package.json (1784 bytes) 257 | UPDATE /src/app/app.module.ts (740 bytes) 258 | ``` 259 | 260 | #### Where to go from here? 261 | 262 | From here, you can run schematics for creating features to get your 263 | project started. For example, to create a `landing` section for your 264 | application, run the following schematic: 265 | 266 | ```bash 267 | schematics @egervari/schematics-angular:feature --name=landing --path=src/app 268 | ``` 269 | 270 | And to create the `home`, `login` and `forgot-password` sections within the 271 | landing module, run the following 3 schematics: 272 | 273 | ```bash 274 | schematics @egervari/schematics-angular:feature --name=home --path=src/app/features/landing 275 | schematics @egervari/schematics-angular:feature --name=login --path=src/app/features/landing 276 | schematics @egervari/schematics-angular:feature --name=forgot-password --path=src/app/features/landing 277 | ``` 278 | 279 | At this point, take a quick look at your source code and you'll be 280 | astounded as to how much manual labour was saved as a result of using 281 | these schematics. Everything is modularized and chained together for 282 | you, such as your routes, lazy-loading, states, state default values and 283 | reducers. Your NgModule's are properly connected. You have clear 284 | separation of concerns everywhere. 285 | 286 | And if you're new to Angular and didn't know the right way to begin 287 | your first project architecturally, this will put you on the right path 288 | to having a great, scalable architecture over the long-term. 289 | 290 | ### Using Schematics Through WebStorm 291 | 292 | To use schematics in WebStorm, right-click on a folder, select 293 | **'New'**, then select **'Angular Schematic'**, and then select any 294 | schematic from the following list: 295 | 296 | ![alt text](docs/webstorm-1.png "Angular Schematics Selection") 297 | 298 | For the parameters input, WebStorm will pass many of the options - such 299 | as project name and path - on your behalf, so all you have to do is set 300 | the `name` parameter for most schematics and you're all set: 301 | 302 | ![alt text](docs/webstorm-2.png "Add name parameter") 303 | 304 | ## Building and testing 305 | 306 | To build the source code, run: 307 | 308 | ```bash 309 | npm run build 310 | ``` 311 | 312 | After the schematics are built, to run them locally from the project 313 | root, run: 314 | 315 | ```bash 316 | cd dist 317 | schematics .:feature --name=app --path=src/app 318 | ``` 319 | 320 | ## Linting and Prettier 321 | 322 | To clean up the source code, run: 323 | 324 | ```bash 325 | npm run fix 326 | ``` 327 | 328 | ## Publishing 329 | 330 | To publish this to npm, run the following commands: 331 | 332 | ```bash 333 | npm version major/minor/patch 334 | npm run build 335 | npm run publish 336 | ``` 337 | 338 | ### Quickly create a patch version 339 | 340 | To quickly create a patch version, instead of going through all of the 341 | build and publish commands manually, instead run: 342 | 343 | ```bash 344 | npm run release-patch 345 | ``` 346 | -------------------------------------------------------------------------------- /docs/webstorm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rangle/schematics-angular/df84d6ee8abc9bffce84e77ef5681df421c8b63f/docs/webstorm-1.png -------------------------------------------------------------------------------- /docs/webstorm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rangle/schematics-angular/df84d6ee8abc9bffce84e77ef5681df421c8b63f/docs/webstorm-2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@egervari/schematics-angular", 3 | "version": "0.2.16", 4 | "description": "Rangle's Angular Schematics to ensure best-practices and scalable architecture", 5 | "keywords": [ 6 | "angular", 7 | "schematics", 8 | "structure", 9 | "folder structure", 10 | "components", 11 | "services", 12 | "ngrx", 13 | "best practices" 14 | ], 15 | "author": "Katie Egervari", 16 | "license": "MIT", 17 | "schematics": "./schematics/collection.json", 18 | "ngPackage": { 19 | "dest": "dist", 20 | "lib": { 21 | "entryFile": "src/public_api.ts" 22 | } 23 | }, 24 | "scripts": { 25 | "build": "ng-packagr -p package.json && npm run build:schematics && npm run copy:schematics", 26 | "build:schematics": "tsc -p tsconfig.json --outDir ./dist", 27 | "copy:schematics": "cpr src dist --overwrite --deleteFirst", 28 | "test": "npm run build && jasmine dist/**/*.spec.js", 29 | "pretty:check": "prettier --list-different \"src/**/*.ts\"", 30 | "pretty:fix": "prettier --write \"src/**/*.ts\"", 31 | "lint:check": "node_modules/.bin/tslint --project ./", 32 | "lint:fix": "node_modules/.bin/tslint --project ./ --fix", 33 | "check": "npm run pretty:check && npm run lint:check", 34 | "fix": "npm run pretty:fix && npm run lint:fix", 35 | "publish": "npm publish dist --access public", 36 | "release-patch": "npm version patch && npm run build && npm run publish" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/core": "^7.0.3", 40 | "@angular-devkit/schematics": "^7.0.3", 41 | "@angular/compiler": "^7.0.1", 42 | "@angular/compiler-cli": "^7.0.1", 43 | "@angular/core": "^7.0.1", 44 | "@schematics/angular": "^7.0.3", 45 | "@types/jasmine": "^2.6.0", 46 | "@types/node": "^10.12.0", 47 | "@types/prettier": "^1.13.2", 48 | "cpr": "^3.0.1", 49 | "jasmine": "^3.3.0", 50 | "ng-packagr": "^4.4.0", 51 | "prettier": "^1.14.3", 52 | "tsickle": "^0.33.1", 53 | "tslint": "^5.11.0", 54 | "tslint-config-prettier": "^1.15.0", 55 | "typescript": "^3.1.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSpacing: true, 4 | printWidth: 100, 5 | semi: true, 6 | singleQuote: true, 7 | tabWidth: 2, 8 | trailingComma: 'none' 9 | }; -------------------------------------------------------------------------------- /src/ast/add-default-value-to-parent-state-functions/add-default-value-to-parent-state-functions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { Folders } from '../../types/folders/folders.enum'; 4 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 5 | 6 | import { addDefaultValueToParentStateFunctions } from './add-default-value-to-parent-state-functions'; 7 | 8 | describe('addDefaultValueToParentStateFunctions()', () => { 9 | let sourceFile: typescript.SourceFile; 10 | 11 | it('add default child state value to the empty parent state create function', () => { 12 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app-state.functions.empty.txt'); 13 | 14 | const modifications = addDefaultValueToParentStateFunctions( 15 | sourceFile, 16 | Folders.Features, 17 | 'stuff' 18 | ); 19 | 20 | expect(modifications.length).toEqual(2); 21 | expect(modifications[0].index).toEqual(49); 22 | expect(modifications[0].toAdd).toEqual( 23 | `import { createStuffState } from '../../features/stuff/types/stuff-state/stuff-state.functions';\n` 24 | ); 25 | expect(modifications[1].index).toEqual(106); 26 | expect(modifications[1].toAdd).toEqual(`stuffState: createStuffState(),`); 27 | }); 28 | 29 | it('add default child state value to the existing parent state create function', () => { 30 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app-state.functions.existing.txt'); 31 | 32 | const modifications = addDefaultValueToParentStateFunctions( 33 | sourceFile, 34 | Folders.Features, 35 | 'stuff' 36 | ); 37 | 38 | expect(modifications.length).toEqual(2); 39 | expect(modifications[0].index).toEqual(139); 40 | expect(modifications[0].toAdd).toEqual( 41 | `import { createStuffState } from '../../features/stuff/types/stuff-state/stuff-state.functions';\n` 42 | ); 43 | expect(modifications[1].index).toEqual(196); 44 | expect(modifications[1].toAdd).toEqual(`stuffState: createStuffState(),`); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/ast/add-default-value-to-parent-state-functions/add-default-value-to-parent-state-functions.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import * as typescript from 'typescript'; 3 | 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | import { addImportStatementToFile } from '../ast-wrappers'; 6 | import { SourceFileModification } from '../source-file-modification.interface'; 7 | 8 | export function addDefaultValueToParentStateFunctions( 9 | sourceFile: typescript.SourceFile, 10 | folderType: Folders, 11 | name: string 12 | ): SourceFileModification[] { 13 | const createFunction = sourceFile.statements.find( 14 | statement => statement.kind === typescript.SyntaxKind.FunctionDeclaration 15 | ) as typescript.FunctionDeclaration; 16 | 17 | if (!createFunction || !createFunction.name.getText().startsWith('create')) { 18 | return []; 19 | } 20 | 21 | const returnStatement = (createFunction.body as typescript.Block).statements.find( 22 | statement => statement.kind === typescript.SyntaxKind.ReturnStatement 23 | ) as typescript.ReturnStatement; 24 | 25 | if (returnStatement.expression.kind !== typescript.SyntaxKind.ObjectLiteralExpression) { 26 | return []; 27 | } 28 | 29 | return [ 30 | addImportStatementToFile( 31 | sourceFile, 32 | `create${strings.classify(name)}State`, 33 | `../..${folderType}/${strings.dasherize(name)}/types/${strings.dasherize( 34 | name 35 | )}-state/${strings.dasherize(name)}-state.functions` 36 | ), 37 | { 38 | index: (returnStatement.expression as typescript.ObjectLiteralExpression).properties.pos, 39 | toAdd: `${strings.camelize(name)}State: create${strings.classify(name)}State(),` 40 | } 41 | ]; 42 | } 43 | -------------------------------------------------------------------------------- /src/ast/add-default-value-to-parent-state-functions/app-state.functions.empty.txt: -------------------------------------------------------------------------------- 1 | import { AppState } from './app-state.interface'; 2 | 3 | export function createAppState(): AppState { 4 | return { 5 | 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/ast/add-default-value-to-parent-state-functions/app-state.functions.existing.txt: -------------------------------------------------------------------------------- 1 | import { childState } from '../../features/child/types/child-state/child-state.function'; 2 | import { AppState } from './app-state.interface'; 3 | 4 | export function createAppState(): AppState { 5 | return { 6 | childState: createChildState(), 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/add-effects-to-module.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 4 | 5 | import { addEffectsToModule } from './add-effects-to-module'; 6 | 7 | describe('addEffectsToModule()', () => { 8 | let sourceFile: typescript.SourceFile; 9 | 10 | it('add NgModule imports property and add EffectsModule', () => { 11 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app.module.no-imports.txt'); 12 | 13 | const modifications = addEffectsToModule(sourceFile, 'Stuff', './store/stuff.effects'); 14 | 15 | expect(modifications.length).toEqual(2); 16 | expect(modifications[0].index).toEqual(87); 17 | expect(modifications[0].toAdd).toEqual(`import { Stuff } from './store/stuff.effects';\n`); 18 | expect(modifications[1].index).toEqual(100); 19 | expect(modifications[1].toAdd).toEqual(`imports: [EffectsModule.forFeature([Stuff])],`); 20 | }); 21 | 22 | it('modify NgModule imports property and add EffectsModule', () => { 23 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app.module.no-effects-module.txt'); 24 | 25 | const modifications = addEffectsToModule(sourceFile, 'Stuff', './store/stuff.effects'); 26 | 27 | expect(modifications.length).toEqual(2); 28 | expect(modifications[0].index).toEqual(203); 29 | expect(modifications[0].toAdd).toEqual(`import { Stuff } from './store/stuff.effects';\n`); 30 | expect(modifications[1].index).toEqual(269); 31 | expect(modifications[1].toAdd).toEqual(`, EffectsModule.forFeature([Stuff])`); 32 | }); 33 | 34 | it('modify NgModule imports property and modify empty EffectsModule', () => { 35 | sourceFile = openSourceFileFromFileSystem( 36 | __dirname + '/app.module.has-empty-effects-module.txt' 37 | ); 38 | 39 | const modifications = addEffectsToModule(sourceFile, 'Stuff', './store/stuff.effects'); 40 | 41 | expect(modifications.length).toEqual(2); 42 | expect(modifications[0].index).toEqual(250); 43 | expect(modifications[0].toAdd).toEqual(`import { Stuff } from './store/stuff.effects';\n`); 44 | expect(modifications[1].index).toEqual(345); 45 | expect(modifications[1].toAdd).toEqual(`Stuff`); 46 | }); 47 | 48 | it('modify NgModule imports property and modify existing EffectsModule', () => { 49 | sourceFile = openSourceFileFromFileSystem( 50 | __dirname + '/app.module.has-existing-effects-module.txt' 51 | ); 52 | 53 | const modifications = addEffectsToModule(sourceFile, 'Stuff', './store/stuff.effects'); 54 | 55 | expect(modifications.length).toEqual(2); 56 | expect(modifications[0].index).toEqual(300); 57 | expect(modifications[0].toAdd).toEqual(`import { Stuff } from './store/stuff.effects';\n`); 58 | expect(modifications[1].index).toEqual(405); 59 | expect(modifications[1].toAdd).toEqual(`, Stuff`); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/add-effects-to-module.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { getNgModuleNode, getObjectProperty, insertIntoArray } from '../ast-helpers'; 4 | import { addImportStatementToFile } from '../ast-wrappers'; 5 | import { SourceFileModification } from '../source-file-modification.interface'; 6 | 7 | export function addEffectsToModule( 8 | sourceFile: typescript.SourceFile, 9 | classifiedName: string, 10 | importPath: string 11 | ): SourceFileModification[] { 12 | const ngModuleNode = getNgModuleNode(sourceFile); 13 | 14 | if (!ngModuleNode) { 15 | return []; 16 | } 17 | 18 | const propertyAssignment = getObjectProperty(ngModuleNode.properties, 'imports'); 19 | 20 | if ( 21 | !propertyAssignment || 22 | propertyAssignment.initializer.kind !== typescript.SyntaxKind.ArrayLiteralExpression 23 | ) { 24 | return [ 25 | addImportStatementToFile(sourceFile, classifiedName, importPath), 26 | { 27 | index: ngModuleNode.properties.pos, 28 | toAdd: `imports: [EffectsModule.forFeature([${classifiedName}])],` 29 | } 30 | ]; 31 | } 32 | 33 | const imports = (propertyAssignment.initializer as typescript.ArrayLiteralExpression).elements; 34 | 35 | const effectsModuleImport = imports.find(element => 36 | element.getText().startsWith('EffectsModule') 37 | ); 38 | 39 | if (effectsModuleImport && effectsModuleImport.kind === typescript.SyntaxKind.CallExpression) { 40 | const forFeatureArguments = (effectsModuleImport as typescript.CallExpression).arguments; 41 | const effects = (forFeatureArguments[0] as typescript.ArrayLiteralExpression).elements; 42 | 43 | return [ 44 | addImportStatementToFile(sourceFile, classifiedName, importPath), 45 | insertIntoArray(effects, classifiedName) 46 | ]; 47 | } else { 48 | return [ 49 | addImportStatementToFile(sourceFile, classifiedName, importPath), 50 | insertIntoArray(imports, `EffectsModule.forFeature([${classifiedName}])`) 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/app.module.has-empty-effects-module.txt: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './components'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | BrowserModule, 11 | AppRoutingModule, 12 | EffectsModule.forRoot([]) 13 | ], 14 | declarations: [ 15 | AppComponent 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule { } 21 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/app.module.has-existing-effects-module.txt: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './components'; 7 | import { AppEffects } from './store/app.effects'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | AppRoutingModule, 13 | EffectsModule.forRoot([AppEffects]) 14 | ], 15 | declarations: [ 16 | AppComponent 17 | ], 18 | providers: [], 19 | bootstrap: [AppComponent] 20 | }) 21 | export class AppModule { } 22 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/app.module.no-effects-module.txt: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './components'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | BrowserModule, 10 | AppRoutingModule 11 | ], 12 | declarations: [ 13 | AppComponent 14 | ], 15 | providers: [], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /src/ast/add-effects-to-module/app.module.no-imports.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppComponent } from './components'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | providers: [], 10 | bootstrap: [AppComponent] 11 | }) 12 | export class AppModule { } 13 | -------------------------------------------------------------------------------- /src/ast/add-reducer-to-parent-reducer/add-reducer-to-parent-reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { Folders } from '../../types/folders/folders.enum'; 4 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 5 | 6 | import { addReducerToParentReducer } from './add-reducer-to-parent-reducer'; 7 | 8 | describe('addReducerToParentReducer()', () => { 9 | let sourceFile: typescript.SourceFile; 10 | 11 | it('add child reducer to the parent app state ActionReducerMap', () => { 12 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app.reducer.txt'); 13 | 14 | const modifications = addReducerToParentReducer(sourceFile, Folders.Features, 'stuff'); 15 | 16 | expect(modifications.length).toEqual(2); 17 | expect(modifications[0].index).toEqual(254); 18 | expect(modifications[0].toAdd).toContain( 19 | `import { stuffReducer } from '../features/stuff/store/stuff.reducer'` 20 | ); 21 | expect(modifications[1].index).toEqual(309); 22 | expect(modifications[1].toAdd).toEqual('stuffState: stuffReducer,'); 23 | }); 24 | 25 | it('add child reducer to the parent feature state reducer that has no compose reducers', () => { 26 | sourceFile = openSourceFileFromFileSystem(__dirname + '/feature.reducer.no-child-states.txt'); 27 | 28 | const modifications = addReducerToParentReducer(sourceFile, Folders.Features, 'child'); 29 | 30 | expect(modifications.length).toEqual(2); 31 | expect(modifications[0].index).toEqual(239); 32 | expect(modifications[0].toAdd).toContain( 33 | `import { childReducer } from '../features/child/store/child.reducer'` 34 | ); 35 | expect(modifications[1].index).toEqual(463); 36 | expect(modifications[1].removeToIndex).toEqual(470); 37 | expect(modifications[1].toAdd).toEqual( 38 | ' { ...state, childState: childReducer(state.childState, action) };' 39 | ); 40 | }); 41 | 42 | it('add child reducer to the parent feature state reducer that has has compose reducers', () => { 43 | sourceFile = openSourceFileFromFileSystem(__dirname + '/feature.reducer.has-child-states.txt'); 44 | 45 | const modifications = addReducerToParentReducer(sourceFile, Folders.Features, 'child'); 46 | 47 | expect(modifications.length).toEqual(2); 48 | expect(modifications[0].index).toEqual(306); 49 | expect(modifications[0].toAdd).toContain( 50 | `import { childReducer } from '../features/child/store/child.reducer'` 51 | ); 52 | expect(modifications[1].index).toEqual(606); 53 | expect(modifications[1].toAdd).toEqual(', childState: childReducer(state.childState, action)'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/ast/add-reducer-to-parent-reducer/add-reducer-to-parent-reducer.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import * as typescript from 'typescript'; 3 | 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | import { getVariableDeclaration } from '../ast-helpers'; 6 | import { addImportStatementToFile } from '../ast-wrappers'; 7 | import { SourceFileModification } from '../source-file-modification.interface'; 8 | 9 | function addChildReducerToAppReducer( 10 | sourceFile: typescript.SourceFile, 11 | folderType: Folders, 12 | name: string, 13 | reducers: typescript.VariableDeclaration 14 | ): SourceFileModification[] { 15 | const actionReducerMap = reducers.initializer as typescript.ObjectLiteralExpression; 16 | 17 | if (actionReducerMap) { 18 | return [ 19 | addImportStatementToFile( 20 | sourceFile, 21 | `${strings.camelize(name)}Reducer`, 22 | `..${folderType}/${strings.dasherize(name)}/store/${strings.dasherize(name)}.reducer` 23 | ), 24 | { 25 | index: actionReducerMap.properties.pos, 26 | toAdd: `${strings.camelize(name)}State: ${strings.camelize(name)}Reducer,` 27 | } 28 | ]; 29 | } 30 | 31 | return []; 32 | } 33 | 34 | function addImportModification( 35 | sourceFile: typescript.SourceFile, 36 | folderType: Folders, 37 | childName: string 38 | ): SourceFileModification { 39 | return addImportStatementToFile( 40 | sourceFile, 41 | `${strings.camelize(childName)}Reducer`, 42 | `..${folderType}/${strings.dasherize(childName)}/store/${strings.dasherize(childName)}.reducer` 43 | ); 44 | } 45 | 46 | function addChildReducerToParentReducer( 47 | sourceFile: typescript.SourceFile, 48 | folderType: Folders, 49 | childName: string 50 | ): SourceFileModification[] { 51 | const reducerFunction = sourceFile.statements.find( 52 | statement => statement.kind === typescript.SyntaxKind.FunctionDeclaration 53 | ) as typescript.FunctionDeclaration; 54 | 55 | const switchStatement = reducerFunction.body.statements.find( 56 | statement => statement.kind === typescript.SyntaxKind.SwitchStatement 57 | ) as typescript.SwitchStatement; 58 | 59 | const defaultClause = switchStatement.caseBlock.clauses.find( 60 | clause => clause.kind === typescript.SyntaxKind.DefaultClause 61 | ) as typescript.DefaultClause; 62 | 63 | const returnStatement = defaultClause.statements.find( 64 | statement => statement.kind === typescript.SyntaxKind.ReturnStatement 65 | ) as typescript.ReturnStatement; 66 | 67 | switch (returnStatement.expression.kind) { 68 | case typescript.SyntaxKind.Identifier: 69 | return [ 70 | addImportModification(sourceFile, folderType, childName), 71 | { 72 | index: returnStatement.expression.pos, 73 | toAdd: ` { ...state, ${strings.camelize(childName)}State: ${strings.camelize( 74 | childName 75 | )}Reducer(state.${strings.camelize(childName)}State, action) };`, 76 | removeToIndex: returnStatement.end 77 | } 78 | ]; 79 | case typescript.SyntaxKind.ObjectLiteralExpression: 80 | const newStateObject = returnStatement.expression as typescript.ObjectLiteralExpression; 81 | 82 | return [ 83 | addImportModification(sourceFile, folderType, childName), 84 | { 85 | index: newStateObject.properties.end, 86 | toAdd: `, ${strings.camelize(childName)}State: ${strings.camelize( 87 | childName 88 | )}Reducer(state.${strings.camelize(childName)}State, action)` 89 | } 90 | ]; 91 | default: 92 | return []; 93 | } 94 | } 95 | 96 | export function addReducerToParentReducer( 97 | sourceFile: typescript.SourceFile, 98 | folderType: Folders, 99 | name: string 100 | ): SourceFileModification[] { 101 | const reducers = getVariableDeclaration(sourceFile, 'ActionReducerMap'); 102 | 103 | return reducers 104 | ? addChildReducerToAppReducer(sourceFile, folderType, name, reducers) 105 | : addChildReducerToParentReducer(sourceFile, folderType, name); 106 | } 107 | -------------------------------------------------------------------------------- /src/ast/add-reducer-to-parent-reducer/app.reducer.txt: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | ActionReducer, 5 | ActionReducerMap, 6 | MetaReducer 7 | } from '@ngrx/store'; 8 | 9 | import { environment } from '../../environments/environment'; 10 | import { AppState } from '../types/app-state/app-state.interface'; 11 | 12 | export const reducers: ActionReducerMap = { 13 | 14 | }; 15 | 16 | 17 | export const metaReducers: Array> = !environment.production ? [] : []; 18 | -------------------------------------------------------------------------------- /src/ast/add-reducer-to-parent-reducer/feature.reducer.has-child-states.txt: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { createStuffState } from '../types/stuff-state/stuff-state.functions'; 4 | import { StuffState } from '../types/stuff-state/stuff-state.interface'; 5 | import { homeReducer } from '../features/home/store/home.reducer'; 6 | 7 | import { StuffActions } from './stuff.actions'; 8 | 9 | export function stuffReducer(state: StuffState = createStuffState(), action: Action): StuffState { 10 | switch (action.type) { 11 | case StuffActions.RETRIEVE: 12 | return { 13 | ...state 14 | }; 15 | default: 16 | return { 17 | ...state, 18 | homeState: homeReducer(state.homeState, action) 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ast/add-reducer-to-parent-reducer/feature.reducer.no-child-states.txt: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { createStuffState } from '../types/stuff-state/stuff-state.functions'; 4 | import { StuffState } from '../types/stuff-state/stuff-state.interface'; 5 | 6 | import { StuffActions } from './stuff.actions'; 7 | 8 | export function stuffReducer(state: StuffState = createStuffState(), action: Action): StuffState { 9 | switch (action.type) { 10 | case StuffActions.RETRIEVE: 11 | return { 12 | ...state 13 | }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ast/add-route-to-routing-module/add-route-to-routing-module.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 4 | import { SourceFileModification } from '../source-file-modification.interface'; 5 | 6 | import { addRouteToRoutingModule } from './add-route-to-routing-module'; 7 | 8 | describe('addRouteToRoutingModule()', () => { 9 | let sourceFile: typescript.SourceFile; 10 | 11 | it('add a route to an empty routes array', () => { 12 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app-routing.module.empty-routes.txt'); 13 | 14 | const modifications: SourceFileModification[] = addRouteToRoutingModule(sourceFile, 'stuff'); 15 | 16 | expect(modifications.length).toEqual(1); 17 | expect(modifications[0].toAdd).toEqual( 18 | `{ path: 'stuff', loadChildren: './features/stuff/stuff.module#StuffModule' }` 19 | ); 20 | expect(modifications[0].index).toEqual(123); 21 | }); 22 | 23 | it('add a route to an existing routes array', () => { 24 | sourceFile = openSourceFileFromFileSystem( 25 | __dirname + '/app-routing.module.existing-routes.txt' 26 | ); 27 | 28 | const modifications: SourceFileModification[] = addRouteToRoutingModule(sourceFile, 'stuff'); 29 | 30 | expect(modifications.length).toEqual(1); 31 | expect(modifications[0].toAdd).toEqual( 32 | `, { path: 'stuff', loadChildren: './features/stuff/stuff.module#StuffModule' }` 33 | ); 34 | expect(modifications[0].index).toEqual(203); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/ast/add-route-to-routing-module/add-route-to-routing-module.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import * as typescript from 'typescript'; 3 | 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | 6 | import { 7 | filterNodeArray, 8 | getObjectProperty, 9 | getVariableDeclaration, 10 | insertIntoArray 11 | } from '../ast-helpers'; 12 | import { SourceFileModification } from '../source-file-modification.interface'; 13 | 14 | export function addRouteToRoutingModule( 15 | sourceFile: typescript.SourceFile, 16 | name: string 17 | ): SourceFileModification[] { 18 | const routesDeclaration = getVariableDeclaration(sourceFile, 'Routes'); 19 | 20 | if (!routesDeclaration) { 21 | return []; 22 | } 23 | 24 | const routes = filterNodeArray( 25 | (routesDeclaration.initializer as typescript.ArrayLiteralExpression) 26 | .elements as typescript.NodeArray, 27 | route => { 28 | const pathProperty = getObjectProperty(route.properties, 'path'); 29 | 30 | return (pathProperty.initializer as typescript.Identifier).text !== '**'; 31 | } 32 | ); 33 | 34 | return [ 35 | insertIntoArray( 36 | routes, 37 | `{ path: '${strings.dasherize(name)}', loadChildren: '.${ 38 | Folders.Features 39 | }/${strings.dasherize(name)}/${strings.dasherize(name)}.module#${strings.classify( 40 | name 41 | )}Module' }` 42 | ) 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /src/ast/add-route-to-routing-module/app-routing.module.empty-routes.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /src/ast/add-route-to-routing-module/app-routing.module.existing-routes.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | redirectTo: 'rentals/listings', 8 | pathMatch: 'full' 9 | }, 10 | { 11 | path: '**', 12 | redirectTo: 'rentals/listings', 13 | pathMatch: 'full' 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { } 22 | -------------------------------------------------------------------------------- /src/ast/add-state-member-to-parent-state-interface/add-state-member-to-parent-state-interface.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { Folders } from '../../types/folders/folders.enum'; 4 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 5 | 6 | import { addStateMemberToParentStateInterface } from './add-state-member-to-parent-state-interface'; 7 | 8 | describe('addStateMemberToParentState()', () => { 9 | let sourceFile: typescript.SourceFile; 10 | 11 | it('add child state as a member to the parent app state interface', () => { 12 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app-state.interface.txt'); 13 | 14 | const modifications = addStateMemberToParentStateInterface( 15 | sourceFile, 16 | Folders.Features, 17 | 'stuff' 18 | ); 19 | 20 | expect(modifications.length).toEqual(2); 21 | expect(modifications[0].index).toEqual(0); 22 | expect(modifications[0].toAdd).toEqual( 23 | `import { StuffState } from '../../features/stuff/types/stuff-state/stuff-state.interface';\n` 24 | ); 25 | expect(modifications[1].index).toEqual(27); 26 | expect(modifications[1].toAdd).toEqual(`stuffState: StuffState;`); 27 | }); 28 | 29 | it('add child state as a member to the parent feature state interface', () => { 30 | sourceFile = openSourceFileFromFileSystem(__dirname + '/feature-state.interface.txt'); 31 | 32 | const modifications = addStateMemberToParentStateInterface( 33 | sourceFile, 34 | Folders.Features, 35 | 'stuff' 36 | ); 37 | 38 | expect(modifications.length).toEqual(2); 39 | expect(modifications[0].index).toEqual(0); 40 | expect(modifications[0].toAdd).toEqual( 41 | `import { StuffState } from '../../features/stuff/types/stuff-state/stuff-state.interface';\n` 42 | ); 43 | expect(modifications[1].index).toEqual(31); 44 | expect(modifications[1].toAdd).toEqual(`stuffState: StuffState;`); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/ast/add-state-member-to-parent-state-interface/add-state-member-to-parent-state-interface.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import * as typescript from 'typescript'; 3 | 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | import { addImportStatementToFile } from '../ast-wrappers'; 6 | import { SourceFileModification } from '../source-file-modification.interface'; 7 | 8 | export function addStateMemberToParentStateInterface( 9 | sourceFile: typescript.SourceFile, 10 | folderType: Folders, 11 | name: string 12 | ): SourceFileModification[] { 13 | const appStateInterface = sourceFile.statements.find( 14 | statement => 15 | statement.kind === typescript.SyntaxKind.InterfaceDeclaration && 16 | (statement as typescript.InterfaceDeclaration).name.getText().endsWith('State') 17 | ) as typescript.InterfaceDeclaration; 18 | 19 | if (appStateInterface) { 20 | return [ 21 | addImportStatementToFile( 22 | sourceFile, 23 | `${strings.classify(name)}State`, 24 | `../..${folderType}/${strings.dasherize(name)}/types/${strings.dasherize( 25 | name 26 | )}-state/${strings.dasherize(name)}-state.interface` 27 | ), 28 | { 29 | index: appStateInterface.members.pos, 30 | toAdd: `${strings.camelize(name)}State: ${strings.classify(name)}State;` 31 | } 32 | ]; 33 | } 34 | 35 | return []; 36 | } 37 | -------------------------------------------------------------------------------- /src/ast/add-state-member-to-parent-state-interface/app-state.interface.txt: -------------------------------------------------------------------------------- 1 | export interface AppState { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/ast/add-state-member-to-parent-state-interface/feature-state.interface.txt: -------------------------------------------------------------------------------- 1 | export interface FeatureState { 2 | removeMe: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/ast/ast-helpers.ts: -------------------------------------------------------------------------------- 1 | import { getDecoratorMetadata } from '@schematics/angular/utility/ast-utils'; 2 | import * as fs from 'fs'; 3 | import * as typescript from 'typescript'; 4 | 5 | import { SourceFileModification } from './source-file-modification.interface'; 6 | 7 | export function openSourceFileFromFileSystem(filename: string) { 8 | return openSourceFile(filename, () => fs.readFileSync(filename, 'utf-8')); 9 | } 10 | 11 | export function openSourceFile(filename: string, readSourceText: () => string) { 12 | if (filename) { 13 | const sourceText = readSourceText(); 14 | 15 | if (sourceText) { 16 | return typescript.createSourceFile( 17 | filename, 18 | sourceText, 19 | typescript.ScriptTarget.Latest, 20 | true 21 | ) as typescript.SourceFile; 22 | } 23 | } 24 | 25 | return null; 26 | } 27 | 28 | export function getNgModuleNode( 29 | sourceFile: typescript.SourceFile 30 | ): typescript.ObjectLiteralExpression { 31 | const nodes = getDecoratorMetadata(sourceFile, 'NgModule', '@angular/core'); 32 | 33 | return nodes.length === 1 && nodes[0].kind === typescript.SyntaxKind.ObjectLiteralExpression 34 | ? (nodes[0] as typescript.ObjectLiteralExpression) 35 | : null; 36 | } 37 | 38 | export function getAllImportDeclarations(sourceFile: typescript.SourceFile) { 39 | return sourceFile.statements.filter( 40 | statement => statement.kind === typescript.SyntaxKind.ImportDeclaration 41 | ); 42 | } 43 | 44 | export function getLastImportDeclaration(sourceFile: typescript.SourceFile) { 45 | return getAllImportDeclarations(sourceFile).reduce((lastDeclaration, declaration) => { 46 | return lastDeclaration 47 | ? lastDeclaration.pos > declaration.pos 48 | ? lastDeclaration 49 | : declaration 50 | : declaration; 51 | }, null); 52 | } 53 | 54 | export function getInterfaceDeclarationByType( 55 | sourceFile: typescript.SourceFile, 56 | type: string 57 | ): typescript.InterfaceDeclaration { 58 | return sourceFile.statements.find( 59 | statement => 60 | statement.kind === typescript.SyntaxKind.InterfaceDeclaration && 61 | (statement as typescript.InterfaceDeclaration).name.getText() === type 62 | ) as typescript.InterfaceDeclaration; 63 | } 64 | 65 | export function getVariableDeclaration( 66 | sourceFile: typescript.SourceFile, 67 | type: string 68 | ): typescript.VariableDeclaration { 69 | return sourceFile.statements 70 | .filter(statement => statement.kind === typescript.SyntaxKind.VariableStatement) 71 | .map(statement => (statement as typescript.VariableStatement).declarationList.declarations) 72 | .filter(declarations => 73 | declarations.some(declaration => { 74 | switch (declaration.type.kind) { 75 | case typescript.SyntaxKind.TypeReference: 76 | return (declaration.type as typescript.TypeReferenceNode).typeName.getText() === type; 77 | case typescript.SyntaxKind.ArrayType: 78 | return ( 79 | ((declaration.type as typescript.ArrayTypeNode) 80 | .elementType as typescript.TypeReferenceNode).typeName.getText() === type 81 | ); 82 | default: 83 | return false; 84 | } 85 | }) 86 | ) 87 | .reduce( 88 | (declaration: typescript.VariableDeclaration, declarations) => 89 | declarations.length > 0 ? declarations[0] : declaration, 90 | null 91 | ); 92 | } 93 | 94 | export function getTypeArgumentOfVariableDeclaration( 95 | variableDeclaration: typescript.VariableDeclaration 96 | ) { 97 | switch (variableDeclaration.type.kind) { 98 | case typescript.SyntaxKind.TypeReference: 99 | return (variableDeclaration.type as typescript.TypeReferenceNode).typeArguments[0]; 100 | case typescript.SyntaxKind.ArrayType: 101 | return ((variableDeclaration.type as typescript.ArrayTypeNode) 102 | .elementType as typescript.TypeReferenceNode).typeArguments[0]; 103 | } 104 | } 105 | 106 | export function getObjectProperty( 107 | properties: typescript.NodeArray, 108 | propertyName: string 109 | ): typescript.PropertyAssignment { 110 | return properties 111 | .filter(property => property.kind === typescript.SyntaxKind.PropertyAssignment) 112 | .filter(property => property.name.kind === typescript.SyntaxKind.Identifier) 113 | .find( 114 | property => (property.name as typescript.Identifier).text === propertyName 115 | ) as typescript.PropertyAssignment; 116 | } 117 | 118 | export function filterNodeArray( 119 | array: typescript.NodeArray, 120 | condition: (node: T) => boolean 121 | ): typescript.NodeArray { 122 | const textRange = { 123 | pos: array.pos, 124 | end: array.end 125 | }; 126 | 127 | const newArray = (array.filter(condition) as {}) as typescript.NodeArray; 128 | newArray.pos = textRange.pos; 129 | newArray.end = textRange.end; 130 | 131 | return newArray; 132 | } 133 | 134 | export function insertIntoArray( 135 | array: typescript.NodeArray, 136 | symbolToInsert: string 137 | ): SourceFileModification { 138 | return { 139 | index: 140 | array.length >= 1 ? array[array.length - 1].end : ((array as {}) as typescript.TextRange).end, 141 | toAdd: array.length >= 1 ? `, ${symbolToInsert}` : symbolToInsert 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /src/ast/ast-wrappers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addDeclarationToModule, 3 | addImportToModule, 4 | addProviderToModule 5 | } from '@schematics/angular/utility/ast-utils'; 6 | import { Change, InsertChange } from '@schematics/angular/utility/change'; 7 | import * as typescript from 'typescript'; 8 | 9 | import { getLastImportDeclaration } from './ast-helpers'; 10 | import { SourceFileModification } from './source-file-modification.interface'; 11 | 12 | function mapInsertChangesToModifications(changes: Change[]) { 13 | return changes.map(mapInsertChangeToModification); 14 | } 15 | 16 | function mapInsertChangeToModification(change: Change) { 17 | const insertChange = change as InsertChange; 18 | 19 | return { 20 | index: insertChange.pos, 21 | toAdd: insertChange.toAdd 22 | }; 23 | } 24 | 25 | export function addImportStatementToFile( 26 | sourceFile: typescript.SourceFile, 27 | symbolToImport: string, 28 | importPath: string 29 | ): SourceFileModification { 30 | const importDeclaration = getLastImportDeclaration(sourceFile); 31 | 32 | return { 33 | index: importDeclaration ? importDeclaration.end : 0, 34 | toAdd: `import { ${symbolToImport} } from '${importPath}';\n` 35 | }; 36 | } 37 | 38 | export function addProviderToNgModule( 39 | sourceFile: typescript.SourceFile, 40 | filename: string, 41 | classifiedName: string, 42 | importPath: string 43 | ): SourceFileModification[] { 44 | return mapInsertChangesToModifications( 45 | addProviderToModule(sourceFile, filename, classifiedName, importPath) 46 | ); 47 | } 48 | 49 | export function addImportToNgModule( 50 | sourceFile: typescript.SourceFile, 51 | filename: string, 52 | classifiedName: string, 53 | importPath: string 54 | ): SourceFileModification[] { 55 | return mapInsertChangesToModifications( 56 | addImportToModule(sourceFile, filename, classifiedName, importPath) 57 | ); 58 | } 59 | 60 | export function addDeclarationToNgModule( 61 | sourceFile: typescript.SourceFile, 62 | filename: string, 63 | classifiedName: string, 64 | importPath: string 65 | ): SourceFileModification[] { 66 | return mapInsertChangesToModifications( 67 | addDeclarationToModule(sourceFile, filename, classifiedName, importPath) 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/ast/rework-app-reducer/app.reducer.txt: -------------------------------------------------------------------------------- 1 | import { 2 | ActionReducer, 3 | ActionReducerMap, 4 | createFeatureSelector, 5 | createSelector, 6 | MetaReducer 7 | } from '@ngrx/store'; 8 | import { environment } from '../../environments/environment'; 9 | 10 | export interface State { 11 | 12 | } 13 | 14 | export const reducers: ActionReducerMap = { 15 | 16 | }; 17 | 18 | 19 | export const metaReducers: MetaReducer[] = !environment.production ? [] : []; 20 | -------------------------------------------------------------------------------- /src/ast/rework-app-reducer/rework-app-reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { openSourceFileFromFileSystem } from '../ast-helpers'; 4 | 5 | import { reworkAppReducer } from './rework-app-reducer'; 6 | 7 | describe('reworkAppReducer()', () => { 8 | let sourceFile: typescript.SourceFile; 9 | 10 | beforeEach(() => { 11 | sourceFile = openSourceFileFromFileSystem(__dirname + '/app.reducer.txt'); 12 | }); 13 | 14 | it('remove State interface and replace State with AppState', () => { 15 | const modifications = reworkAppReducer(sourceFile); 16 | 17 | expect(modifications.length).toEqual(4); 18 | expect(modifications[0].index).toEqual(186); 19 | expect(modifications[0].toAdd).toContain( 20 | `import { AppState } from '../types/app-state/app-state.interface'` 21 | ); 22 | expect(modifications[1].index).toEqual(186); 23 | expect(modifications[1].removeToIndex).toEqual(215); 24 | expect(modifications[2].index).toEqual(257); 25 | expect(modifications[2].removeToIndex).toEqual(262); 26 | expect(modifications[2].toAdd).toEqual('AppState'); 27 | expect(modifications[3].index).toEqual(313); 28 | expect(modifications[3].removeToIndex).toEqual(318); 29 | expect(modifications[3].toAdd).toEqual('AppState'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/ast/rework-app-reducer/rework-app-reducer.ts: -------------------------------------------------------------------------------- 1 | import * as typescript from 'typescript'; 2 | 3 | import { 4 | getInterfaceDeclarationByType, 5 | getTypeArgumentOfVariableDeclaration, 6 | getVariableDeclaration 7 | } from '../ast-helpers'; 8 | import { addImportStatementToFile } from '../ast-wrappers'; 9 | import { SourceFileModification } from '../source-file-modification.interface'; 10 | 11 | export function reworkAppReducer(sourceFile: typescript.SourceFile): SourceFileModification[] { 12 | const modifications = [ 13 | addImportStatementToFile(sourceFile, 'AppState', '../types/app-state/app-state.interface') 14 | ]; 15 | 16 | const stateDeclaration = getInterfaceDeclarationByType(sourceFile, 'State'); 17 | 18 | if (stateDeclaration) { 19 | modifications.push({ 20 | index: stateDeclaration.pos, 21 | removeToIndex: stateDeclaration.end 22 | }); 23 | } 24 | 25 | const reducers = getVariableDeclaration(sourceFile, 'ActionReducerMap'); 26 | 27 | if (reducers) { 28 | const typeArgument = getTypeArgumentOfVariableDeclaration(reducers); 29 | 30 | modifications.push({ 31 | index: typeArgument.pos, 32 | removeToIndex: typeArgument.end, 33 | toAdd: 'AppState' 34 | }); 35 | } 36 | 37 | const metaReducers = getVariableDeclaration(sourceFile, 'MetaReducer'); 38 | 39 | if (metaReducers) { 40 | const typeArgument = getTypeArgumentOfVariableDeclaration(metaReducers); 41 | modifications.push({ 42 | index: typeArgument.pos, 43 | removeToIndex: typeArgument.end, 44 | toAdd: 'AppState' 45 | }); 46 | } 47 | 48 | return modifications; 49 | } 50 | -------------------------------------------------------------------------------- /src/ast/source-file-modification.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SourceFileModification { 2 | index: number; 3 | toAdd?: string; 4 | removeToIndex?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './schematics'; 2 | -------------------------------------------------------------------------------- /src/rules/add-npm-dependencies.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | import { addPackageJsonDependency, NodeDependency } from '@schematics/angular/utility/dependencies'; 3 | 4 | export function addNpmDependenciesRule(dependencies: NodeDependency[]): Rule { 5 | return (tree: Tree, context: SchematicContext) => { 6 | dependencies.forEach(dependency => { 7 | addPackageJsonDependency(tree, dependency); 8 | 9 | context.logger.log('info', `Added "${dependency.name}" into ${dependency.type}`); 10 | }); 11 | 12 | return tree; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/rules/add-npm-scripts.rule.ts: -------------------------------------------------------------------------------- 1 | import { parseJson } from '@angular-devkit/core'; 2 | import { Rule, Tree } from '@angular-devkit/schematics'; 3 | 4 | import { NpmScript } from '../types/npm-script/npm-script.interface'; 5 | 6 | const PackageJsonFilename = 'package.json'; 7 | 8 | export function addNpmScriptsRule(scripts: NpmScript[]): Rule { 9 | return (tree: Tree) => { 10 | const packageJson = tree.read(PackageJsonFilename); 11 | 12 | if (packageJson) { 13 | const json = parseJson(packageJson.toString('utf-8')) as any; 14 | 15 | if (!json.scripts) { 16 | json.scripts = {}; 17 | } 18 | 19 | scripts.forEach(script => (json.scripts[script.name] = script.command)); 20 | 21 | tree.overwrite(PackageJsonFilename, JSON.stringify(json, null, 2)); 22 | } 23 | 24 | return tree; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/rules/delete-files.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Tree } from '@angular-devkit/schematics'; 2 | 3 | import { deleteFile } from './tree-helpers'; 4 | 5 | export function deleteFilesRule(files: string[]): Rule { 6 | return (tree: Tree) => { 7 | files.forEach(file => deleteFile(tree, file)); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/rules/modify-source-file.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Tree } from '@angular-devkit/schematics'; 2 | import * as typescript from 'typescript'; 3 | 4 | import { SourceFileModification } from '../ast/source-file-modification.interface'; 5 | 6 | import { applyModificationsToTreeFile, openSourceFileFromTree } from './tree-helpers'; 7 | 8 | export function modifySourceFile( 9 | getFilename: (tree: Tree) => string, 10 | computeModifications: ( 11 | sourceFile: typescript.SourceFile, 12 | filename: string 13 | ) => SourceFileModification[] 14 | ): Rule { 15 | return (tree: Tree) => { 16 | const filename = getFilename(tree); 17 | 18 | if (filename) { 19 | const sourceFile = openSourceFileFromTree(tree, filename); 20 | 21 | if (sourceFile) { 22 | applyModificationsToTreeFile(tree, filename, computeModifications(sourceFile, filename)); 23 | } 24 | } 25 | 26 | return tree; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/rules/process-templates.rule.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { 3 | apply, 4 | branchAndMerge, 5 | mergeWith, 6 | move, 7 | template, 8 | url, 9 | Rule 10 | } from '@angular-devkit/schematics'; 11 | 12 | import { PathOptions } from '../types/path-options/path-options.interface'; 13 | 14 | export function processTemplates( 15 | options: T, 16 | directory: string = options.path 17 | ): Rule { 18 | return branchAndMerge( 19 | mergeWith( 20 | apply(url('./files'), [ 21 | template({ 22 | ...strings, 23 | ...{ uppercase: (value: string) => value.toUpperCase() }, 24 | ...(options as {}) 25 | }), 26 | move(directory) 27 | ]) 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/rules/run-npm-install.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext } from '@angular-devkit/schematics'; 2 | import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; 3 | 4 | export function runNpmInstallRule(): Rule { 5 | return (_, context: SchematicContext) => { 6 | context.addTask(new NodePackageInstallTask()); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/rules/run-prettier.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Tree } from '@angular-devkit/schematics'; 2 | import * as prettier from 'prettier'; 3 | 4 | /* tslint:disable-next-line */ 5 | const prettierConfig = require('../schematics/tslint-and-prettier/files/prettier.config'); 6 | 7 | import { getTouchedFiles } from './tree-helpers'; 8 | 9 | export function runPrettier(): Rule { 10 | return (tree: Tree) => { 11 | getTouchedFiles(tree).forEach(file => { 12 | tree.overwrite( 13 | file, 14 | prettier.format(tree.read(file).toString(), { 15 | ...prettierConfig, 16 | parser: 'typescript' 17 | }) 18 | ); 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/rules/run-tslint-fix.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | import { TslintFixTask } from '@angular-devkit/schematics/tasks'; 3 | 4 | import { PathOptions } from '../types/path-options/path-options.interface'; 5 | 6 | import { findFilenameInTree, getTouchedFiles } from './tree-helpers'; 7 | 8 | function getTslintJsonFilename(tree: Tree, options: PathOptions) { 9 | return `.${findFilenameInTree(tree.getDir(options.path), file => file.includes('tslint.json'))}`; 10 | } 11 | 12 | export function runTslintFix(options: PathOptions): Rule { 13 | return (tree: Tree, context: SchematicContext) => { 14 | context.addTask( 15 | new TslintFixTask({ 16 | tslintPath: getTslintJsonFilename(tree, options), 17 | files: getTouchedFiles(tree), 18 | ignoreErrors: true 19 | }) 20 | ); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/rules/tree-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { DirEntry, FileEntry, Tree } from '@angular-devkit/schematics'; 3 | import * as typescript from 'typescript'; 4 | 5 | import { openSourceFile } from '../ast/ast-helpers'; 6 | import { SourceFileModification } from '../ast/source-file-modification.interface'; 7 | import { SchemaOptions } from '../types/schema-options/schema-options.interface'; 8 | 9 | export function deleteFile(tree: Tree, filename: string) { 10 | if (tree.exists(filename)) { 11 | tree.delete(filename); 12 | } 13 | } 14 | 15 | export function getSubDirEntry(directory: DirEntry, subDirectoryNames: string[]): DirEntry { 16 | if (subDirectoryNames.length > 0) { 17 | const subDirectoryPath = directory.subdirs.find(dir => dir === subDirectoryNames[0]); 18 | 19 | if (subDirectoryPath) { 20 | const subDirectory = directory.dir(subDirectoryPath); 21 | 22 | return subDirectoryNames.length > 1 23 | ? getSubDirEntry(subDirectory, subDirectoryNames.slice(1)) 24 | : subDirectory; 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | 31 | export function getSubFileEntry(directory: DirEntry, subFileName: string): FileEntry { 32 | const stateTypeFilePath = directory.subfiles.find(file => file.includes(subFileName)); 33 | 34 | return stateTypeFilePath ? directory.file(stateTypeFilePath) : null; 35 | } 36 | 37 | export function getSubDirFileEntry( 38 | directory: DirEntry, 39 | subDirectoryNames: string[], 40 | subFileName: string 41 | ): FileEntry { 42 | const subDirEntry = getSubDirEntry(directory, subDirectoryNames); 43 | 44 | if (subDirEntry) { 45 | const subFileEntry = getSubFileEntry(subDirEntry, subFileName); 46 | 47 | if (subFileEntry) { 48 | return subFileEntry; 49 | } 50 | } 51 | 52 | return null; 53 | } 54 | 55 | export function findFilenameInTree( 56 | directory: DirEntry, 57 | fileMatchesCriteria: (file: string) => boolean 58 | ): string { 59 | const pathFragment = directory.subfiles.find(fileMatchesCriteria); 60 | 61 | if (pathFragment) { 62 | const fileEntry = directory.file(pathFragment); 63 | 64 | if (fileEntry) { 65 | return fileEntry.path; 66 | } 67 | } 68 | 69 | return directory.parent ? findFilenameInTree(directory.parent, fileMatchesCriteria) : null; 70 | } 71 | 72 | export function findModuleFilenameInTree(tree: Tree, options: SchemaOptions): string { 73 | return findFilenameInTree( 74 | tree.getDir(options.path), 75 | file => file.endsWith('.module.ts') && !file.includes('-routing') 76 | ); 77 | } 78 | 79 | export function findParentModuleFilenameInTree(tree: Tree, options: SchemaOptions): string { 80 | return findFilenameInTree( 81 | tree.getDir(options.path), 82 | file => 83 | file.endsWith('.module.ts') && 84 | !file.includes('-routing') && 85 | file !== `${strings.dasherize(options.name)}.module.ts` 86 | ); 87 | } 88 | 89 | export function findParentRoutingModuleFilenameInTree(tree: Tree, options: SchemaOptions): string { 90 | return findFilenameInTree( 91 | tree.getDir(options.path), 92 | file => 93 | file.endsWith('-routing.module.ts') && 94 | file !== `${strings.dasherize(options.name)}-routing.module.ts` 95 | ); 96 | } 97 | 98 | export function openSourceFileFromTree(tree: Tree, filename: string): typescript.SourceFile { 99 | return openSourceFile(filename, () => tree.read(filename).toString('utf-8')); 100 | } 101 | 102 | export function applyModificationsToTreeFile( 103 | tree: Tree, 104 | filename: string, 105 | modifications: SourceFileModification[] 106 | ) { 107 | const updateRecorder = tree.beginUpdate(filename); 108 | 109 | modifications.forEach(modification => { 110 | if (modification.removeToIndex) { 111 | updateRecorder.remove(modification.index, modification.removeToIndex - modification.index); 112 | } 113 | 114 | if (modification.toAdd) { 115 | updateRecorder.insertLeft(modification.index, modification.toAdd); 116 | } 117 | }); 118 | 119 | tree.commitUpdate(updateRecorder); 120 | } 121 | 122 | export function getTouchedFiles(tree: Tree) { 123 | return tree.actions.reduce((files, action) => { 124 | return action.path.endsWith('.ts') ? files.concat([`.${action.path}`]) : files; 125 | }, []); 126 | } 127 | -------------------------------------------------------------------------------- /src/rules/update-barrel-file.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Tree } from '@angular-devkit/schematics'; 2 | 3 | export function updateBarrelFile(path: string, newContent: string | Buffer): Rule { 4 | return (tree: Tree) => { 5 | const barrelFile = path + '/index.ts'; 6 | 7 | if (!tree.exists(barrelFile)) { 8 | tree.create(barrelFile, newContent); 9 | } else { 10 | const existingContent = tree.read(barrelFile); 11 | 12 | if (existingContent) { 13 | tree.overwrite(barrelFile, existingContent.toString() + newContent); 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "initialize": { 5 | "description": "Run this schematic after creating a new project with the Angular CLI", 6 | "factory": "./initialize", 7 | "schema": "./initialize/schema.json" 8 | }, 9 | "tslint-and-prettier": { 10 | "description": "Add TSLint and Prettier. Creates/replaces TSLint and Prettier settings with recommended configuration", 11 | "factory": "./tslint-and-prettier", 12 | "schema": "./tslint-and-prettier/schema.json" 13 | }, 14 | "feature": { 15 | "description": "Create a feature module, complete with module, routes, root component, service, state interface and ngrx infrastructure", 16 | "factory": "./feature", 17 | "schema": "./feature/schema.json" 18 | }, 19 | "module": { 20 | "description": "Create a module that contains its own ngrx infrastructure without routes and components", 21 | "factory": "./module", 22 | "schema": "./module/schema.json" 23 | }, 24 | "component": { 25 | "description": "Create a component", 26 | "factory": "./component", 27 | "schema": "./component/schema.json" 28 | }, 29 | "service": { 30 | "description": "Create a service", 31 | "factory": "./service", 32 | "schema": "./service/schema.json" 33 | }, 34 | "type": { 35 | "description": "Create the type with interface and functions files", 36 | "factory": "./type", 37 | "schema": "./type/schema.json" 38 | }, 39 | "ngrx": { 40 | "description": "Create all @ngrx files for a feature", 41 | "factory": "./ngrx", 42 | "schema": "./ngrx/schema.json" 43 | }, 44 | "ngrx-store": { 45 | "description": "Create an @ngrx store service for a feature", 46 | "factory": "./ngrx-store", 47 | "schema": "./ngrx-store/schema.json" 48 | }, 49 | "ngrx-actions": { 50 | "description": "Create an @ngrx actions file for a feature", 51 | "factory": "./ngrx-actions", 52 | "schema": "./ngrx-actions/schema.json" 53 | }, 54 | "ngrx-reducer": { 55 | "description": "Create an @ngrx reducer function for a feature", 56 | "factory": "./ngrx-reducer", 57 | "schema": "./ngrx-reducer/schema.json" 58 | }, 59 | "ngrx-effects": { 60 | "description": "Create an @ngrx effects service for a feature", 61 | "factory": "./ngrx-effects", 62 | "schema": "./ngrx-effects/schema.json" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/schematics/component/files/__name@dasherize__/__name@dasherize__.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rangle/schematics-angular/df84d6ee8abc9bffce84e77ef5681df421c8b63f/src/schematics/component/files/__name@dasherize__/__name@dasherize__.component.html -------------------------------------------------------------------------------- /src/schematics/component/files/__name@dasherize__/__name@dasherize__.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | 3 | } -------------------------------------------------------------------------------- /src/schematics/component/files/__name@dasherize__/__name@dasherize__.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: '<%= prefix %>-<%= name %>', 5 | templateUrl: './<%= name %>.component.html', 6 | styleUrls: ['./<%= name %>.component.scss'] 7 | }) 8 | export class <%= classify(name) %>Component { 9 | 10 | } -------------------------------------------------------------------------------- /src/schematics/component/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, Rule, Tree } from '@angular-devkit/schematics'; 3 | 4 | import { addDeclarationToNgModule } from '../../ast/ast-wrappers'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { findModuleFilenameInTree } from '../../rules/tree-helpers'; 8 | import { updateBarrelFile } from '../../rules/update-barrel-file.rule'; 9 | import { Folders } from '../../types/folders/folders.enum'; 10 | import { getProjectPrefix } from '../../types/project-schema-options/project-schema-options.functions'; 11 | import { ProjectSchemaOptions } from '../../types/project-schema-options/project-schema-options.interface'; 12 | import { 13 | getContainingFolderPath, 14 | validateRegularSchema 15 | } from '../../types/schema-options/schema-options.functions'; 16 | 17 | export default function(options: ProjectSchemaOptions): Rule { 18 | validateRegularSchema(options); 19 | 20 | options.path = getContainingFolderPath(options.path, Folders.Components); 21 | 22 | return chain([ 23 | (tree: Tree) => { 24 | options.prefix = getProjectPrefix(tree, options); 25 | 26 | return processTemplates(options, options.path); 27 | }, 28 | updateBarrelFile( 29 | options.path, 30 | `export * from './${strings.dasherize(options.name)}/${strings.dasherize( 31 | options.name 32 | )}.component';\r\n` 33 | ), 34 | modifySourceFile( 35 | tree => findModuleFilenameInTree(tree, options), 36 | (sourceFile, moduleFilename) => 37 | addDeclarationToNgModule( 38 | sourceFile, 39 | moduleFilename, 40 | strings.classify(`${options.name}Component`), 41 | `.${Folders.Components}` 42 | ) 43 | ) 44 | ]); 45 | } 46 | -------------------------------------------------------------------------------- /src/schematics/component/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Component", 4 | "title": "Component Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the component.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "name": { 14 | "description": "The name of the component.", 15 | "type": "string", 16 | "$default": { 17 | "$source": "argv", 18 | "index": 0 19 | }, 20 | "x-prompt": "What name would you like to use for the component?" 21 | }, 22 | "prefix": { 23 | "description": "The prefix to apply to generated selectors.", 24 | "type": "string", 25 | "format": "html-selector", 26 | "alias": "p" 27 | } 28 | }, 29 | "required": [ 30 | "name" 31 | ] 32 | } -------------------------------------------------------------------------------- /src/schematics/feature/files/__name@dasherize__-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { <%= classify(name) %>Component } from './components'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: <%= classify(name) %>Component, 10 | }, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forChild(routes)], 15 | exports: [RouterModule], 16 | }) 17 | export class <%= classify(name) %>RoutingModule {} 18 | -------------------------------------------------------------------------------- /src/schematics/feature/files/__name@dasherize__.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { EffectsModule } from '@ngrx/effects'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule, 9 | RouterModule 10 | ], 11 | declarations: [], 12 | exports: [], 13 | providers: [] 14 | }) 15 | export class <%= classify(name) %>Module {} 16 | -------------------------------------------------------------------------------- /src/schematics/feature/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, schematic, Rule, Tree } from '@angular-devkit/schematics'; 3 | 4 | import { addRouteToRoutingModule } from '../../ast/add-route-to-routing-module/add-route-to-routing-module'; 5 | import { addImportToNgModule } from '../../ast/ast-wrappers'; 6 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 7 | import { processTemplates } from '../../rules/process-templates.rule'; 8 | import { runPrettier } from '../../rules/run-prettier.rule'; 9 | import { runTslintFix } from '../../rules/run-tslint-fix.rule'; 10 | import { 11 | findModuleFilenameInTree, 12 | findParentRoutingModuleFilenameInTree 13 | } from '../../rules/tree-helpers'; 14 | import { Folders } from '../../types/folders/folders.enum'; 15 | import { getProjectPrefix } from '../../types/project-schema-options/project-schema-options.functions'; 16 | import { ProjectSchemaOptions } from '../../types/project-schema-options/project-schema-options.interface'; 17 | import { 18 | getContainingFolderPath, 19 | validateRegularSchema 20 | } from '../../types/schema-options/schema-options.functions'; 21 | 22 | export default function(options: ProjectSchemaOptions): Rule { 23 | validateRegularSchema(options); 24 | 25 | return chain([ 26 | (tree: Tree) => { 27 | options.path = `${getContainingFolderPath(options.path, Folders.Features)}/${options.name}`; 28 | options.prefix = getProjectPrefix(tree, options); 29 | 30 | return chain([ 31 | processTemplates(options), 32 | schematic('component', options), 33 | schematic('service', options), 34 | schematic('type', { 35 | ...options, 36 | name: `${options.name}-state` 37 | }), 38 | schematic('ngrx', options) 39 | ]); 40 | }, 41 | modifySourceFile( 42 | tree => findModuleFilenameInTree(tree, options), 43 | (sourceFile, moduleFilename) => 44 | addImportToNgModule( 45 | sourceFile, 46 | moduleFilename, 47 | strings.classify(`${options.name}RoutingModule`), 48 | `./${strings.dasherize(options.name)}-routing.module` 49 | ) 50 | ), 51 | modifySourceFile( 52 | tree => findParentRoutingModuleFilenameInTree(tree, options), 53 | sourceFile => addRouteToRoutingModule(sourceFile, options.name) 54 | ), 55 | runPrettier(), 56 | runTslintFix(options) 57 | ]); 58 | } 59 | -------------------------------------------------------------------------------- /src/schematics/feature/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Feature", 4 | "title": "Feature Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "project": { 14 | "type": "string", 15 | "description": "The name of the project.", 16 | "$default": { 17 | "$source": "projectName" 18 | } 19 | }, 20 | "name": { 21 | "description": "The name of the feature.", 22 | "type": "string", 23 | "$default": { 24 | "$source": "argv", 25 | "index": 0 26 | }, 27 | "x-prompt": "What name would you like to use for the feature?" 28 | }, 29 | "prefix": { 30 | "description": "The prefix to apply to generated selectors.", 31 | "type": "string", 32 | "format": "html-selector", 33 | "alias": "p" 34 | } 35 | }, 36 | "required": [ 37 | "name", "path" 38 | ] 39 | } -------------------------------------------------------------------------------- /src/schematics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component'; 2 | export * from './feature'; 3 | export * from './initialize'; 4 | export * from './module'; 5 | export * from './ngrx'; 6 | export * from './ngrx-actions'; 7 | export * from './ngrx-effects'; 8 | export * from './ngrx-reducer'; 9 | export * from './ngrx-store'; 10 | export * from './service'; 11 | export * from './tslint-and-prettier'; 12 | export * from './type'; 13 | -------------------------------------------------------------------------------- /src/schematics/initialize/files/app.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { select, Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AppState } from '../types/app-state/app-state.interface'; 6 | 7 | @Injectable() 8 | export class AppStore { 9 | constructor(private store: Store) {} 10 | 11 | public getAppState(): Observable { 12 | return this.store.pipe(select(state => state)); 13 | } 14 | } -------------------------------------------------------------------------------- /src/schematics/initialize/index.ts: -------------------------------------------------------------------------------- 1 | import { chain, schematic, Rule, Tree } from '@angular-devkit/schematics'; 2 | 3 | import { addProviderToNgModule } from '../../ast/ast-wrappers'; 4 | import { reworkAppReducer } from '../../ast/rework-app-reducer/rework-app-reducer'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { updateBarrelFile } from '../../rules/update-barrel-file.rule'; 8 | 9 | const AppModule = 'src/app/app.module.ts'; 10 | const AppReducer = 'src/app/store/app.reducer.ts'; 11 | 12 | export default function(): Rule { 13 | return chain([ 14 | schematic('tslint-and-prettier', {}), 15 | schematic('type', { 16 | path: 'src/app', 17 | name: 'app-state', 18 | addMember: false 19 | }), 20 | updateBarrelFile('src/app/components', `export * from './app/app.component'`), 21 | (tree: Tree) => { 22 | const filesToMove = [ 23 | { 24 | from: 'src/app/app.component.html', 25 | to: 'src/app/components/app/app.component.html' 26 | }, 27 | { 28 | from: 'src/app/app.component.scss', 29 | to: 'src/app/components/app/app.component.scss' 30 | }, 31 | { 32 | from: 'src/app/app.component.ts', 33 | to: 'src/app/components/app/app.component.ts' 34 | }, 35 | { 36 | from: 'src/app/reducers/index.ts', 37 | to: AppReducer 38 | }, 39 | { 40 | from: 'src/app/app.effects.ts', 41 | to: 'src/app/store/app.effects.ts' 42 | } 43 | ]; 44 | 45 | filesToMove.forEach(filename => { 46 | if (tree.exists(filename.from) && !tree.exists(filename.to)) { 47 | tree.create(filename.to, tree.read(filename.from)); 48 | tree.delete(filename.from); 49 | } 50 | }); 51 | }, 52 | (tree: Tree) => { 53 | const filesToDelete = [ 54 | 'src/app/app.component.spec.ts', 55 | 'src/app/app.effects.spec.ts', 56 | 'src/app/reducers' 57 | ]; 58 | 59 | filesToDelete.forEach(filename => { 60 | if (tree.exists(filename)) { 61 | tree.delete(filename); 62 | } 63 | }); 64 | }, 65 | (tree: Tree) => { 66 | if (tree.exists(AppModule)) { 67 | tree.overwrite( 68 | AppModule, 69 | tree 70 | .read(AppModule) 71 | .toString() 72 | .replace(`'./app.component'`, `'./components'`) 73 | .replace(`'./app.effects'`, `'./store/app.effects'`) 74 | .replace(`'./reducers'`, `'./store/app.reducer'`) 75 | ); 76 | } 77 | }, 78 | processTemplates({ 79 | path: 'src/app/store' 80 | }), 81 | (tree: Tree) => { 82 | if (tree.exists(AppModule)) { 83 | return modifySourceFile( 84 | () => AppModule, 85 | (sourceFile, moduleFilename) => 86 | addProviderToNgModule(sourceFile, moduleFilename, 'AppStore', `./store/app.store`) 87 | ); 88 | } 89 | }, 90 | (tree: Tree) => { 91 | if (tree.exists(AppReducer)) { 92 | return modifySourceFile(() => AppReducer, sourceFile => reworkAppReducer(sourceFile)); 93 | } 94 | } 95 | ]); 96 | } 97 | -------------------------------------------------------------------------------- /src/schematics/initialize/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Initialize", 4 | "title": "Initialize Schema", 5 | "type": "object", 6 | "properties": {} 7 | } -------------------------------------------------------------------------------- /src/schematics/module/files/__name@dasherize__.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { EffectsModule } from '@ngrx/effects'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule, 9 | RouterModule 10 | ], 11 | providers: [] 12 | }) 13 | export class <%= classify(name) %>Module {} 14 | -------------------------------------------------------------------------------- /src/schematics/module/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, schematic, Rule } from '@angular-devkit/schematics'; 3 | 4 | import { addImportToNgModule } from '../../ast/ast-wrappers'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { runPrettier } from '../../rules/run-prettier.rule'; 8 | import { runTslintFix } from '../../rules/run-tslint-fix.rule'; 9 | import { findParentModuleFilenameInTree } from '../../rules/tree-helpers'; 10 | import { Folders } from '../../types/folders/folders.enum'; 11 | import { 12 | getContainingFolderPath, 13 | validateRegularSchema 14 | } from '../../types/schema-options/schema-options.functions'; 15 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 16 | 17 | export default function(options: SchemaOptions): Rule { 18 | validateRegularSchema(options); 19 | 20 | options.path = `${getContainingFolderPath(options.path, Folders.Modules)}/${options.name}`; 21 | 22 | return chain([ 23 | processTemplates(options), 24 | schematic('service', options), 25 | schematic('type', { 26 | ...options, 27 | name: `${options.name}-state` 28 | }), 29 | schematic('ngrx', options), 30 | modifySourceFile( 31 | tree => findParentModuleFilenameInTree(tree, options), 32 | (sourceFile, moduleFilename) => 33 | addImportToNgModule( 34 | sourceFile, 35 | moduleFilename, 36 | strings.classify(`${options.name}Module`), 37 | `.${Folders.Modules}/${strings.dasherize(options.name)}/${strings.dasherize( 38 | options.name 39 | )}.module` 40 | ) 41 | ), 42 | runPrettier(), 43 | runTslintFix(options) 44 | ]); 45 | } 46 | -------------------------------------------------------------------------------- /src/schematics/module/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Module", 4 | "title": "Module Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "name": { 14 | "description": "The name of the feature.", 15 | "type": "string", 16 | "$default": { 17 | "$source": "argv", 18 | "index": 0 19 | }, 20 | "x-prompt": "What name would you like to use for the feature?" 21 | } 22 | }, 23 | "required": [ 24 | "name", "path" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/schematics/ngrx-actions/files/__name@dasherize__.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export const <%= classify(name) %>Actions = { 4 | RETRIEVE: '<%= uppercase(underscore(name)) %>--RETRIEVE' 5 | }; 6 | 7 | export class Retrieve implements Action { 8 | public readonly type = <%= classify(name) %>Actions.RETRIEVE; 9 | } 10 | -------------------------------------------------------------------------------- /src/schematics/ngrx-actions/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@angular-devkit/schematics'; 2 | 3 | import { processTemplates } from '../../rules/process-templates.rule'; 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | import { 6 | getContainingFolderPath, 7 | validateRegularSchema 8 | } from '../../types/schema-options/schema-options.functions'; 9 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 10 | 11 | export default function(options: SchemaOptions): Rule { 12 | validateRegularSchema(options); 13 | 14 | options.path = getContainingFolderPath(options.path, Folders.Store); 15 | 16 | return processTemplates(options, options.path); 17 | } 18 | -------------------------------------------------------------------------------- /src/schematics/ngrx-actions/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NgrxActions", 4 | "title": "Ngrx Actions Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "default": "src" 11 | }, 12 | "name": { 13 | "description": "The name of the feature.", 14 | "type": "string", 15 | "$default": { 16 | "$source": "argv", 17 | "index": 0 18 | }, 19 | "x-prompt": "What name would you like to use for the feature?" 20 | } 21 | }, 22 | "required": [ 23 | "name", "path" 24 | ] 25 | } -------------------------------------------------------------------------------- /src/schematics/ngrx-effects/files/__name@dasherize__.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ofType, Actions, Effect } from '@ngrx/effects'; 3 | import { of } from 'rxjs'; 4 | import { catchError, map, switchMap } from 'rxjs/operators'; 5 | 6 | import { <%= classify( name ) %>Actions } from './<%= dasherize( name ) %>.actions'; 7 | 8 | @Injectable() 9 | export class <%= classify( name ) %>Effects { 10 | 11 | constructor( 12 | private actions$: Actions 13 | ) {} 14 | 15 | @Effect() 16 | private retrieve<%= classify(name) %>$ = this.actions$.pipe( 17 | ofType(<%= classify(name) %>Actions.RETRIEVE) 18 | ); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/schematics/ngrx-effects/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, Rule } from '@angular-devkit/schematics'; 3 | 4 | import { addEffectsToModule } from '../../ast/add-effects-to-module/add-effects-to-module'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { findModuleFilenameInTree } from '../../rules/tree-helpers'; 8 | import { Folders } from '../../types/folders/folders.enum'; 9 | import { 10 | getContainingFolderPath, 11 | validateRegularSchema 12 | } from '../../types/schema-options/schema-options.functions'; 13 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 14 | 15 | export default function(options: SchemaOptions): Rule { 16 | validateRegularSchema(options); 17 | 18 | options.path = getContainingFolderPath(options.path, Folders.Store); 19 | 20 | return chain([ 21 | processTemplates(options, options.path), 22 | modifySourceFile( 23 | tree => findModuleFilenameInTree(tree, options), 24 | sourceFile => 25 | addEffectsToModule( 26 | sourceFile, 27 | strings.classify(`${options.name}Effects`), 28 | `.${Folders.Store}/${strings.dasherize(options.name)}.effects` 29 | ) 30 | ) 31 | ]); 32 | } 33 | -------------------------------------------------------------------------------- /src/schematics/ngrx-effects/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NgrxEffects", 4 | "title": "Ngrx Effects Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "default": "src" 11 | }, 12 | "name": { 13 | "description": "The name of the feature.", 14 | "type": "string", 15 | "$default": { 16 | "$source": "argv", 17 | "index": 0 18 | }, 19 | "x-prompt": "What name would you like to use for the feature?" 20 | } 21 | }, 22 | "required": [ 23 | "name", "path" 24 | ] 25 | } -------------------------------------------------------------------------------- /src/schematics/ngrx-reducer/files/__name@dasherize__.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { create<%= classify(name) %>State } from '../types/<%= dasherize(name) %>-state/<%= dasherize(name) %>-state.functions'; 4 | import { <%= classify(name) %>State } from '../types/<%= dasherize(name) %>-state/<%= dasherize(name) %>-state.interface'; 5 | 6 | import { <%= classify(name) %>Actions } from './<%= dasherize(name) %>.actions'; 7 | 8 | export function <%= camelize(name) %>Reducer( 9 | state: <%= classify(name) %>State = create<%= classify(name) %>State(), 10 | action: Action 11 | ): <%= classify(name) %>State { 12 | switch (action.type) { 13 | case <%= classify(name) %>Actions.RETRIEVE: 14 | return { 15 | ...state 16 | }; 17 | default: 18 | return state; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/schematics/ngrx-reducer/index.ts: -------------------------------------------------------------------------------- 1 | import { chain, DirEntry, Rule } from '@angular-devkit/schematics'; 2 | 3 | import { addDefaultValueToParentStateFunctions } from '../../ast/add-default-value-to-parent-state-functions/add-default-value-to-parent-state-functions'; 4 | import { addReducerToParentReducer } from '../../ast/add-reducer-to-parent-reducer/add-reducer-to-parent-reducer'; 5 | import { addStateMemberToParentStateInterface } from '../../ast/add-state-member-to-parent-state-interface/add-state-member-to-parent-state-interface'; 6 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 7 | import { processTemplates } from '../../rules/process-templates.rule'; 8 | import { getSubDirEntry } from '../../rules/tree-helpers'; 9 | import { Folders } from '../../types/folders/folders.enum'; 10 | import { getFolderType } from '../../types/path-options/path-options.functions'; 11 | import { 12 | getContainingFolderPath, 13 | validateRegularSchema 14 | } from '../../types/schema-options/schema-options.functions'; 15 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 16 | 17 | function findParentReducerFile(directory: DirEntry, name: string): string { 18 | const storeDirEntry = getSubDirEntry(directory, ['store']); 19 | 20 | if (storeDirEntry) { 21 | const reducerPath = storeDirEntry.subfiles.find( 22 | file => file.includes('.reducer.ts') && file !== `${name}.reducer.ts` 23 | ); 24 | 25 | if (reducerPath) { 26 | return storeDirEntry.file(reducerPath).path; 27 | } 28 | } 29 | 30 | return directory.parent ? findParentReducerFile(directory.parent, name) : null; 31 | } 32 | 33 | function findParentStateTypesFile(directory: DirEntry, extension: string, name: string): string { 34 | const typesDirEntry = getSubDirEntry(directory, ['types']); 35 | 36 | if (typesDirEntry) { 37 | const statePath = typesDirEntry.subdirs.find(dir => dir.includes('-state')); 38 | 39 | if (statePath) { 40 | const stateDirEntry = typesDirEntry.dir(statePath); 41 | 42 | const stateTypeFilePath = stateDirEntry.subfiles.find( 43 | file => file.includes(`-state.${extension}.ts`) && file !== `${name}-state.${extension}.ts` 44 | ); 45 | 46 | if (stateTypeFilePath) { 47 | return stateDirEntry.file(stateTypeFilePath).path; 48 | } 49 | } 50 | } 51 | 52 | return directory.parent ? findParentStateTypesFile(directory.parent, extension, name) : null; 53 | } 54 | 55 | export default function(options: SchemaOptions): Rule { 56 | validateRegularSchema(options); 57 | 58 | options.path = getContainingFolderPath(options.path, Folders.Store); 59 | 60 | const folderType = getFolderType(options); 61 | 62 | return chain([ 63 | processTemplates(options, options.path), 64 | modifySourceFile( 65 | tree => findParentReducerFile(tree.getDir(options.path), options.name), 66 | sourceFile => addReducerToParentReducer(sourceFile, folderType, options.name) 67 | ), 68 | modifySourceFile( 69 | tree => findParentStateTypesFile(tree.getDir(options.path), 'interface', options.name), 70 | sourceFile => addStateMemberToParentStateInterface(sourceFile, folderType, options.name) 71 | ), 72 | modifySourceFile( 73 | tree => findParentStateTypesFile(tree.getDir(options.path), 'functions', options.name), 74 | sourceFile => addDefaultValueToParentStateFunctions(sourceFile, folderType, options.name) 75 | ) 76 | ]); 77 | } 78 | -------------------------------------------------------------------------------- /src/schematics/ngrx-reducer/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NgrxReducer", 4 | "title": "Ngrx Reducer Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "default": "src" 11 | }, 12 | "name": { 13 | "description": "The name of the feature.", 14 | "type": "string", 15 | "$default": { 16 | "$source": "argv", 17 | "index": 0 18 | }, 19 | "x-prompt": "What name would you like to use for the feature?" 20 | } 21 | }, 22 | "required": [ 23 | "name", "path" 24 | ] 25 | } -------------------------------------------------------------------------------- /src/schematics/ngrx-store/files/__name@dasherize__.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | <% if (appStateInterfacePath) { %> import { AppState } from '<%= appStateInterfacePath %>';<% } %> 6 | 7 | import { <%= classify(parentName) %>Store } from '../../../store/<%= dasherize(parentName) %>.store'; 8 | import { <%= classify(name) %>State } from '../types/<%= dasherize(name) %>-state/<%= dasherize(name) %>-state.interface'; 9 | 10 | import { Retrieve } from './<%= dasherize(name) %>.actions'; 11 | 12 | @Injectable() 13 | export class <%= classify(name) %>Store { 14 | 15 | constructor( 16 | <% if (parentName) { %> private <%= camelize(parentName) %>Store: <%= classify(parentName) %>Store, <% } %> 17 | private store: Store 18 | ) {} 19 | <% if (parentName) { %> 20 | 21 | public get<%= classify(name) %>State(): Observable<<%= classify(name) %>State> { 22 | return this.<%= camelize(parentName) %>Store.get<%= classify(parentName) %>State().pipe(map(state => state.<%= camelize(name) %>State)); 23 | } 24 | <% } %> 25 | 26 | public retrieve() { 27 | this.store.dispatch(new Retrieve()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/schematics/ngrx-store/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, DirEntry, Rule, Tree } from '@angular-devkit/schematics'; 3 | 4 | import { addProviderToNgModule } from '../../ast/ast-wrappers'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { findModuleFilenameInTree, getSubDirFileEntry } from '../../rules/tree-helpers'; 8 | import { Folders } from '../../types/folders/folders.enum'; 9 | import { findParentModuleName } from '../../types/path-options/path-options.functions'; 10 | import { 11 | getContainingFolderPath, 12 | validateRegularSchema 13 | } from '../../types/schema-options/schema-options.functions'; 14 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 15 | 16 | function findAppStateTypesFile(directory: DirEntry): string { 17 | const appStateInterfaceFileEntry = getSubDirFileEntry( 18 | directory, 19 | ['types', 'app-state'], 20 | 'app-state.interface.ts' 21 | ); 22 | 23 | if (appStateInterfaceFileEntry) { 24 | return appStateInterfaceFileEntry.path.replace(/\.ts$/gi, '').slice(1); 25 | } 26 | 27 | return directory.parent ? findAppStateTypesFile(directory.parent) : null; 28 | } 29 | 30 | export default function(options: SchemaOptions): Rule { 31 | validateRegularSchema(options); 32 | 33 | options.path = getContainingFolderPath(options.path, Folders.Store); 34 | 35 | return chain([ 36 | (tree: Tree) => { 37 | return processTemplates( 38 | { 39 | ...options, 40 | appStateInterfacePath: findAppStateTypesFile(tree.getDir(options.path)), 41 | parentName: findParentModuleName(options) 42 | }, 43 | options.path 44 | ); 45 | }, 46 | modifySourceFile( 47 | tree => findModuleFilenameInTree(tree, options), 48 | (sourceFile, moduleFilename) => 49 | addProviderToNgModule( 50 | sourceFile, 51 | moduleFilename, 52 | strings.classify(`${options.name}Store`), 53 | `.${Folders.Store}/${strings.dasherize(options.name)}.store` 54 | ) 55 | ) 56 | ]); 57 | } 58 | -------------------------------------------------------------------------------- /src/schematics/ngrx-store/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NgrxStore", 4 | "title": "Ngrx Store Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "default": "src" 11 | }, 12 | "name": { 13 | "description": "The name of the feature.", 14 | "type": "string", 15 | "$default": { 16 | "$source": "argv", 17 | "index": 0 18 | }, 19 | "x-prompt": "What name would you like to use for the feature?" 20 | } 21 | }, 22 | "required": [ 23 | "name", "path" 24 | ] 25 | } -------------------------------------------------------------------------------- /src/schematics/ngrx/index.ts: -------------------------------------------------------------------------------- 1 | import { chain, schematic, Rule } from '@angular-devkit/schematics'; 2 | 3 | import { validateRegularSchema } from '../../types/schema-options/schema-options.functions'; 4 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 5 | 6 | export default function(options: SchemaOptions): Rule { 7 | validateRegularSchema(options); 8 | 9 | return chain([ 10 | schematic('ngrx-actions', options), 11 | schematic('ngrx-effects', options), 12 | schematic('ngrx-reducer', options), 13 | schematic('ngrx-store', options) 14 | ]); 15 | } 16 | -------------------------------------------------------------------------------- /src/schematics/ngrx/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Ngrx", 4 | "title": "Ngrx Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "name": { 14 | "description": "The name of the feature.", 15 | "type": "string", 16 | "$default": { 17 | "$source": "argv", 18 | "index": 0 19 | }, 20 | "x-prompt": "What name would you like to use for the feature?" 21 | } 22 | }, 23 | "required": [ 24 | "name", "path" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/schematics/service/files/__name@dasherize__.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | @Injectable() 5 | export class <%= classify(name) %>Service { 6 | 7 | constructor(private httpClient: HttpClient) {} 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/schematics/service/index.ts: -------------------------------------------------------------------------------- 1 | import * as strings from '@angular-devkit/core/src/utils/strings'; 2 | import { chain, Rule } from '@angular-devkit/schematics'; 3 | 4 | import { addProviderToNgModule } from '../../ast/ast-wrappers'; 5 | import { modifySourceFile } from '../../rules/modify-source-file.rule'; 6 | import { processTemplates } from '../../rules/process-templates.rule'; 7 | import { findModuleFilenameInTree } from '../../rules/tree-helpers'; 8 | import { Folders } from '../../types/folders/folders.enum'; 9 | import { 10 | getContainingFolderPath, 11 | validateRegularSchema 12 | } from '../../types/schema-options/schema-options.functions'; 13 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 14 | 15 | export default function(options: SchemaOptions): Rule { 16 | validateRegularSchema(options); 17 | 18 | options.path = getContainingFolderPath(options.path, Folders.Services); 19 | 20 | return chain([ 21 | processTemplates(options, options.path), 22 | modifySourceFile( 23 | tree => findModuleFilenameInTree(tree, options), 24 | (sourceFile, moduleFilename) => 25 | addProviderToNgModule( 26 | sourceFile, 27 | moduleFilename, 28 | strings.classify(`${options.name}Service`), 29 | `.${Folders.Services}/${strings.dasherize(options.name)}.service` 30 | ) 31 | ) 32 | ]); 33 | } 34 | -------------------------------------------------------------------------------- /src/schematics/service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Service", 4 | "title": "Service Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "name": { 14 | "description": "The name of the feature.", 15 | "type": "string", 16 | "$default": { 17 | "$source": "argv", 18 | "index": 0 19 | }, 20 | "x-prompt": "What name would you like to use for the feature?" 21 | } 22 | }, 23 | "required": [ 24 | "name", "path" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/schematics/tslint-and-prettier/files/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSpacing: true, 4 | printWidth: 100, 5 | semi: true, 6 | singleQuote: true, 7 | tabWidth: 2, 8 | trailingComma: 'none' 9 | }; 10 | -------------------------------------------------------------------------------- /src/schematics/tslint-and-prettier/files/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "defaultSeverity": "error", 6 | "extends": [ 7 | "tslint:recommended", 8 | "tslint-config-prettier" 9 | ], 10 | "target": "es2017", 11 | "lib": [ 12 | "es2017" 13 | ], 14 | "rules": { 15 | "no-console": false, 16 | "no-namespace": false, 17 | "object-literal-sort-keys": false, 18 | "interface-name" : [true, "never-prefix"], 19 | "cyclomatic-complexity": [ 20 | true, 21 | 10 22 | ], 23 | "eofline": false, 24 | "jsdoc-format": false, 25 | "max-classes-per-file": [false], 26 | "ordered-imports": [ 27 | true, 28 | { 29 | "import-sources-order": "lowercase-first", 30 | "named-imports-order": "lowercase-first", 31 | "grouped-imports": true 32 | } 33 | ], 34 | "quotemark": [ 35 | true, 36 | "single", 37 | "avoid-escape" 38 | ], 39 | "trailing-comma": [ 40 | false 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/schematics/tslint-and-prettier/index.ts: -------------------------------------------------------------------------------- 1 | import { chain, Rule } from '@angular-devkit/schematics'; 2 | import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; 3 | 4 | import { addNpmDependenciesRule } from '../../rules/add-npm-dependencies.rule'; 5 | import { addNpmScriptsRule } from '../../rules/add-npm-scripts.rule'; 6 | import { deleteFilesRule } from '../../rules/delete-files.rule'; 7 | import { processTemplates } from '../../rules/process-templates.rule'; 8 | import { runNpmInstallRule } from '../../rules/run-npm-install.rule'; 9 | import { PathOptions } from '../../types/path-options/path-options.interface'; 10 | 11 | export default function(options: PathOptions): Rule { 12 | return chain([ 13 | deleteFilesRule(['/tslint.json', '/prettier.config.js']), 14 | processTemplates(options, '/'), 15 | addNpmDependenciesRule([ 16 | { type: NodeDependencyType.Dev, version: '^1.14.3', name: 'prettier' }, 17 | { type: NodeDependencyType.Dev, version: '^1.15.0', name: 'tslint-config-prettier' } 18 | ]), 19 | addNpmScriptsRule([ 20 | { name: 'pretty:check', command: 'prettier --list-different "src/**/*.ts"' }, 21 | { name: 'pretty:fix', command: 'prettier --write "src/**/*.ts"' }, 22 | { name: 'lint:check', command: 'ng lint' }, 23 | { name: 'lint:fix', command: 'ng lint --fix' }, 24 | { name: 'check', command: 'npm run pretty:check && npm run lint:check' }, 25 | { name: 'fix', command: 'npm run pretty:fix && npm run lint:fix' } 26 | ]), 27 | runNpmInstallRule() 28 | ]); 29 | } 30 | -------------------------------------------------------------------------------- /src/schematics/tslint-and-prettier/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "TslintAndPrettier", 4 | "title": "TSLint & Prettier Schema", 5 | "type": "object", 6 | "properties": {} 7 | } -------------------------------------------------------------------------------- /src/schematics/type/files/__name@dasherize__/__name@dasherize__.functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.interface'; 2 | 3 | describe('<%= classify(name) %>', () => { 4 | let <%= camelize(name) %>: <%= classify(name) %>; 5 | 6 | describe('when the <%= classify(name) %> is in a certain state', () => { 7 | beforeEach(() => { 8 | <%= camelize(name) %> = {} as <%= classify(name) %>; 9 | }); 10 | 11 | it('should be true', () => { 12 | expect(<%= camelize(name) %>).toEqual({} as <%= classify(name) %>); 13 | }); 14 | }); 15 | }); -------------------------------------------------------------------------------- /src/schematics/type/files/__name@dasherize__/__name@dasherize__.functions.ts: -------------------------------------------------------------------------------- 1 | import { <%= classify(name) %> } from './<%= dasherize(name) %>.interface'; 2 | 3 | export function create<%= classify(name) %>(): <%= classify(name) %> { 4 | return { 5 | <% if(addMember) { %> removeMe: null <% } %> 6 | }; 7 | } -------------------------------------------------------------------------------- /src/schematics/type/files/__name@dasherize__/__name@dasherize__.interface.ts: -------------------------------------------------------------------------------- 1 | export interface <%= classify(name) %> { 2 | <% if(addMember) { %> removeMe: number; <% } %> 3 | } 4 | -------------------------------------------------------------------------------- /src/schematics/type/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@angular-devkit/schematics'; 2 | 3 | import { processTemplates } from '../../rules/process-templates.rule'; 4 | import { Folders } from '../../types/folders/folders.enum'; 5 | import { 6 | getContainingFolderPath, 7 | validateRegularSchema 8 | } from '../../types/schema-options/schema-options.functions'; 9 | 10 | import { TypeSchemaOptions } from './type-schema-options.interface'; 11 | 12 | export default function(options: TypeSchemaOptions): Rule { 13 | validateRegularSchema(options); 14 | 15 | options.path = getContainingFolderPath(options.path, Folders.Types); 16 | 17 | return processTemplates(options, options.path); 18 | } 19 | -------------------------------------------------------------------------------- /src/schematics/type/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "Type", 4 | "title": "Type Schema", 5 | "type": "object", 6 | "properties": { 7 | "path": { 8 | "description": "The path to create the feature.", 9 | "type": "string", 10 | "format": "path", 11 | "visible": false 12 | }, 13 | "name": { 14 | "description": "The name of the feature.", 15 | "type": "string", 16 | "$default": { 17 | "$source": "argv", 18 | "index": 0 19 | }, 20 | "x-prompt": "What name would you like to use for the feature?" 21 | }, 22 | "addMember": { 23 | "description": "Add a 'removeMe' member to the interface as a placeholder", 24 | "type": "boolean", 25 | "default": true 26 | } 27 | }, 28 | "required": [ 29 | "name", "path" 30 | ] 31 | } -------------------------------------------------------------------------------- /src/schematics/type/type-schema-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { SchemaOptions } from '../../types/schema-options/schema-options.interface'; 2 | 3 | export interface TypeSchemaOptions extends SchemaOptions { 4 | addMember: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/folders/folders.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Folders { 2 | Components = '/components', 3 | Features = '/features', 4 | Modules = '/modules', 5 | Services = '/services', 6 | Store = '/store', 7 | Types = '/types' 8 | } 9 | -------------------------------------------------------------------------------- /src/types/npm-script/npm-script.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NpmScript { 2 | name: string; 3 | command: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/path-options/path-options.functions.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from '@angular-devkit/core'; 2 | 3 | import { Folders } from '../folders/folders.enum'; 4 | 5 | import { PathOptions } from './path-options.interface'; 6 | 7 | function getReversedPathFragments(options: PathOptions) { 8 | return normalize(options.path) 9 | .split('/') 10 | .reverse(); 11 | } 12 | 13 | export function getFolderType(options: PathOptions): Folders { 14 | for (const pathFragment of getReversedPathFragments(options)) { 15 | switch (`/${pathFragment}`) { 16 | case Folders.Modules: 17 | return Folders.Modules; 18 | } 19 | } 20 | 21 | return Folders.Features; 22 | } 23 | 24 | export function findParentModuleName(options: PathOptions): string { 25 | const pathFragments = getReversedPathFragments(options); 26 | 27 | const featuresOrModulesIndex = pathFragments.findIndex(pathFragment => { 28 | switch (`/${pathFragment}`) { 29 | case Folders.Features: 30 | case Folders.Modules: 31 | return true; 32 | default: 33 | return false; 34 | } 35 | }); 36 | 37 | return featuresOrModulesIndex + 1 <= pathFragments.length 38 | ? pathFragments[featuresOrModulesIndex + 1] 39 | : null; 40 | } 41 | -------------------------------------------------------------------------------- /src/types/path-options/path-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PathOptions { 2 | path: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/project-schema-options/project-schema-options.functions.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { getWorkspace } from '@schematics/angular/utility/config'; 3 | 4 | const defaultPrefix = 'app'; 5 | 6 | import { ProjectSchemaOptions } from './project-schema-options.interface'; 7 | 8 | export function getProject(tree: Tree, options: ProjectSchemaOptions) { 9 | return getWorkspace(tree).projects[options.project]; 10 | } 11 | 12 | export function getProjectPrefix(tree: Tree, options: ProjectSchemaOptions) { 13 | if (options.prefix) { 14 | return options.prefix; 15 | } 16 | 17 | try { 18 | const project = getProject(tree, options); 19 | 20 | return project && project.prefix ? project.prefix : defaultPrefix; 21 | } catch (noWorkspaceError) { 22 | return defaultPrefix; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/types/project-schema-options/project-schema-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { SchemaOptions } from '../schema-options/schema-options.interface'; 2 | 3 | export interface ProjectSchemaOptions extends SchemaOptions { 4 | prefix: string; 5 | project: string; 6 | selector: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/schema-options/schema-options.functions.ts: -------------------------------------------------------------------------------- 1 | import { SchematicsException } from '@angular-devkit/schematics'; 2 | 3 | import { SchemaOptions } from './schema-options.interface'; 4 | 5 | export function validateRegularSchema(options: SchemaOptions) { 6 | if (!options.name) { 7 | throw new SchematicsException('Option (name) is required.'); 8 | } 9 | 10 | if (!options.path) { 11 | throw new SchematicsException('Option (path) is required.'); 12 | } 13 | } 14 | 15 | export function getContainingFolderPath(path: string, folder: string) { 16 | return path.endsWith(folder) ? path : `${path}${folder}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/types/schema-options/schema-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { PathOptions } from '../path-options/path-options.interface'; 2 | 3 | export interface SchemaOptions extends PathOptions { 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "tsconfig", 4 | "lib": [ 5 | "es2017", 6 | "dom" 7 | ], 8 | "declaration": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "rootDir": "src/", 18 | "skipDefaultLibCheck": true, 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "strictNullChecks": false, // This is annoying to deal with due to no auto complete in WebStorm 22 | "target": "es6", 23 | "types": [ 24 | "jasmine", 25 | "node" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*" 30 | ], 31 | "exclude": [ 32 | "src/**/files/**/*" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-config-prettier" 6 | ], 7 | "target": "es2017", 8 | "lib": [ 9 | "es2017" 10 | ], 11 | "rules": { 12 | "no-console": false, 13 | "no-namespace": false, 14 | "object-literal-sort-keys": false, 15 | "interface-name" : [true, "never-prefix"], 16 | "cyclomatic-complexity": [ 17 | true, 18 | 10 19 | ], 20 | "eofline": false, 21 | "jsdoc-format": false, 22 | "max-classes-per-file": [false], 23 | "ordered-imports": [ 24 | true, 25 | { 26 | "import-sources-order": "lowercase-first", 27 | "named-imports-order": "lowercase-first", 28 | "grouped-imports": true 29 | } 30 | ], 31 | "quotemark": [ 32 | true, 33 | "single", 34 | "avoid-escape" 35 | ], 36 | "trailing-comma": [ 37 | false 38 | ] 39 | } 40 | } --------------------------------------------------------------------------------