├── .circleci └── config.yml ├── .commitlintrc.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.yml │ ├── Feature_request.yml │ ├── Regression.yml │ └── config.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierrc ├── .release-it.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── actions ├── abstract.action.ts ├── add.action.ts ├── build.action.ts ├── generate.action.ts ├── index.ts ├── info.action.ts ├── new.action.ts └── start.action.ts ├── bin └── nest.ts ├── commands ├── abstract.command.ts ├── add.command.ts ├── build.command.ts ├── command.input.ts ├── command.loader.ts ├── generate.command.ts ├── index.ts ├── info.command.ts ├── new.command.ts └── start.command.ts ├── gulpfile.js ├── lib ├── compiler │ ├── assets-manager.ts │ ├── base-compiler.ts │ ├── compiler.ts │ ├── defaults │ │ ├── swc-defaults.ts │ │ └── webpack-defaults.ts │ ├── helpers │ │ ├── append-extension.ts │ │ ├── copy-path-resolve.ts │ │ ├── delete-out-dir.ts │ │ ├── get-builder.ts │ │ ├── get-tsc-config.path.ts │ │ ├── get-value-or-default.ts │ │ ├── get-webpack-config-path.ts │ │ ├── manual-restart.ts │ │ └── tsconfig-provider.ts │ ├── hooks │ │ └── tsconfig-paths.hook.ts │ ├── interfaces │ │ └── readonly-visitor.interface.ts │ ├── plugins │ │ ├── plugin-metadata-generator.ts │ │ ├── plugin-metadata-printer.ts │ │ └── plugins-loader.ts │ ├── swc │ │ ├── constants.ts │ │ ├── forked-type-checker.ts │ │ ├── swc-compiler.ts │ │ └── type-checker-host.ts │ ├── typescript-loader.ts │ ├── watch-compiler.ts │ └── webpack-compiler.ts ├── configuration │ ├── configuration.loader.ts │ ├── configuration.ts │ ├── defaults.ts │ ├── index.ts │ └── nest-configuration.loader.ts ├── package-managers │ ├── abstract.package-manager.ts │ ├── index.ts │ ├── npm.package-manager.ts │ ├── package-manager-commands.ts │ ├── package-manager.factory.ts │ ├── package-manager.ts │ ├── pnpm.package-manager.ts │ ├── project.dependency.ts │ └── yarn.package-manager.ts ├── questions │ └── questions.ts ├── readers │ ├── file-system.reader.ts │ ├── index.ts │ └── reader.ts ├── runners │ ├── abstract.runner.ts │ ├── git.runner.ts │ ├── index.ts │ ├── npm.runner.ts │ ├── pnpm.runner.ts │ ├── runner.factory.ts │ ├── runner.ts │ ├── schematic.runner.ts │ └── yarn.runner.ts ├── schematics │ ├── abstract.collection.ts │ ├── collection.factory.ts │ ├── collection.ts │ ├── custom.collection.ts │ ├── index.ts │ ├── nest.collection.ts │ └── schematic.option.ts ├── ui │ ├── banner.ts │ ├── emojis.ts │ ├── errors.ts │ ├── index.ts │ ├── messages.ts │ └── prefixes.ts └── utils │ ├── formatting.ts │ ├── get-default-tsconfig-path.ts │ ├── gracefully-exit-on-prompt-error.ts │ ├── is-module-available.ts │ ├── load-configuration.ts │ ├── local-binaries.ts │ ├── os-info.utils.ts │ ├── project-utils.ts │ ├── remaining-flags.ts │ ├── tree-kill.ts │ └── type-assertions.ts ├── package-lock.json ├── package.json ├── renovate.json ├── test ├── actions │ └── info.action.spec.ts ├── jest-config.json └── lib │ ├── compiler │ ├── helpers │ │ └── get-value-or-default.spec.ts │ ├── hooks │ │ ├── __snapshots__ │ │ │ └── tsconfig-paths.hook.spec.ts.snap │ │ ├── fixtures │ │ │ ├── aliased-imports │ │ │ │ └── src │ │ │ │ │ ├── bar.tsx │ │ │ │ │ ├── baz.js │ │ │ │ │ ├── foo.ts │ │ │ │ │ ├── main.ts │ │ │ │ │ └── qux.jsx │ │ │ ├── type-imports │ │ │ │ └── src │ │ │ │ │ ├── main.ts │ │ │ │ │ ├── type-a.ts │ │ │ │ │ ├── type-b.ts │ │ │ │ │ ├── type-c.ts │ │ │ │ │ └── type-d.ts │ │ │ └── unused-imports │ │ │ │ └── src │ │ │ │ ├── bar.ts │ │ │ │ ├── foo.ts │ │ │ │ └── main.ts │ │ └── tsconfig-paths.hook.spec.ts │ └── swc │ │ └── swc-compiler.spec.ts │ ├── configuration │ └── nest-configuration.loader.spec.ts │ ├── package-managers │ ├── npm.package-manager.spec.ts │ ├── package-manager.factory.spec.ts │ ├── pnpm.package-manager.spec.ts │ └── yarn.package-manager.spec.ts │ ├── questions │ └── questions.spec.ts │ ├── readers │ └── file-system.reader.spec.ts │ ├── runners │ └── schematic.runner.spec.ts │ ├── schematics │ ├── custom.collection.spec.ts │ ├── fixtures │ │ ├── extended │ │ │ └── collection.json │ │ ├── package │ │ │ ├── a │ │ │ │ └── b │ │ │ │ │ └── c │ │ │ │ │ └── collection.json │ │ │ ├── index.js │ │ │ └── package.json │ │ └── simple │ │ │ └── collection.json │ ├── nest.collection.spec.ts │ └── schematic.option.spec.ts │ └── utils │ └── get-default-tsconfig-path.spec.ts ├── tools └── gulp │ ├── config.ts │ ├── gulpfile.ts │ ├── tasks │ └── clean.ts │ ├── tsconfig.json │ └── util │ └── task-helpers.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache 5 | restore_cache: 6 | key: dependency-cache-{{ checksum "package.json" }} 7 | - &install-deps 8 | run: 9 | name: Install dependencies 10 | command: npm install --ignore-scripts --legacy-peer-deps 11 | - &build-packages 12 | run: 13 | name: Build 14 | command: npm run build 15 | - &run-unit-tests 16 | run: 17 | name: Test 18 | command: npm run test -- --runInBand --no-cache 19 | 20 | jobs: 21 | build: 22 | working_directory: ~/nest 23 | docker: 24 | - image: cimg/node:22.14.0 25 | steps: 26 | - checkout 27 | - restore_cache: 28 | key: dependency-cache-{{ checksum "package.json" }} 29 | - run: 30 | name: Install dependencies 31 | command: npm install --ignore-scripts --legacy-peer-deps 32 | - save_cache: 33 | key: dependency-cache-{{ checksum "package.json" }} 34 | paths: 35 | - ./node_modules 36 | - run: 37 | name: Build 38 | command: npm run build 39 | 40 | unit_tests: 41 | working_directory: ~/nest 42 | docker: 43 | - image: cimg/node:22.14.0 44 | steps: 45 | - checkout 46 | - *restore-cache 47 | - *install-deps 48 | - *build-packages 49 | - run: 50 | name: Clean build artifacts 51 | command: npm run clean 52 | - *run-unit-tests 53 | 54 | workflows: 55 | version: 2 56 | build-and-test: 57 | jobs: 58 | - build 59 | - unit_tests: 60 | requires: 61 | - build 62 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | src/**/*.test.ts 3 | src/**/files/** 4 | test/** 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier' 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/no-use-before-define': 'off', 23 | '@typescript-eslint/no-non-null-assertion': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage", "bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Is there an existing issue for this?" 23 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 24 | options: 25 | - label: "I have searched the existing issues" 26 | required: true 27 | 28 | - type: textarea 29 | validations: 30 | required: true 31 | attributes: 32 | label: "Current behavior" 33 | description: "How the issue manifests?" 34 | 35 | - type: input 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Minimum reproduction code" 40 | description: "An URL to some git repository or gist that reproduces this issue. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction)" 41 | placeholder: "https://github.com/..." 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Steps to reproduce" 46 | description: | 47 | How the issue manifests? 48 | You could leave this blank if you alread write this in your reproduction code/repo 49 | placeholder: | 50 | 1. `npm i` 51 | 2. `npm start:dev` 52 | 3. See error... 53 | 54 | - type: textarea 55 | validations: 56 | required: true 57 | attributes: 58 | label: "Expected behavior" 59 | description: "A clear and concise description of what you expected to happend (or code)" 60 | 61 | - type: markdown 62 | attributes: 63 | value: | 64 | --- 65 | 66 | - type: input 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Package version" 71 | description: | 72 | Which version of `@nestjs/cli` are you using? 73 | **Tip**: Make sure that all of yours `@nestjs/*` dependencies are in sync! 74 | placeholder: "8.1.3" 75 | 76 | - type: input 77 | attributes: 78 | label: "NestJS version" 79 | description: "Which version of `@nestjs/core` are you using?" 80 | placeholder: "8.1.3" 81 | 82 | - type: input 83 | attributes: 84 | label: "Node.js version" 85 | description: "Which version of Node.js are you using?" 86 | placeholder: "14.17.6" 87 | 88 | - type: checkboxes 89 | attributes: 90 | label: "In which operating systems have you tested?" 91 | options: 92 | - label: macOS 93 | - label: Windows 94 | - label: Linux 95 | 96 | - type: markdown 97 | attributes: 98 | value: | 99 | --- 100 | 101 | - type: textarea 102 | attributes: 103 | label: "Other" 104 | description: | 105 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 106 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 107 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | --- 17 | 18 | - type: checkboxes 19 | attributes: 20 | label: "Is there an existing issue that is already proposing this?" 21 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 22 | options: 23 | - label: "I have searched the existing issues" 24 | required: true 25 | 26 | - type: textarea 27 | validations: 28 | required: true 29 | attributes: 30 | label: "Is your feature request related to a problem? Please describe it" 31 | description: "A clear and concise description of what the problem is" 32 | placeholder: | 33 | I have an issue when ... 34 | 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Describe the solution you'd like" 40 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 41 | 42 | - type: textarea 43 | attributes: 44 | label: "Teachability, documentation, adoption, migration strategy" 45 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design?" 46 | 47 | - type: textarea 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What is the motivation / use case for changing the behavior?" 52 | description: "Describe the motivation or the concrete use case" 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Regression.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 Regression" 2 | description: "Report an unexpected behavior while upgrading your Nest application!" 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Did you read the migration guide?" 23 | description: "Check out the [migration guide here](https://docs.nestjs.com/migration-guide)!" 24 | options: 25 | - label: "I have read the whole migration guide" 26 | required: false 27 | 28 | - type: checkboxes 29 | attributes: 30 | label: "Is there an existing issue that is already proposing this?" 31 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 32 | options: 33 | - label: "I have searched the existing issues" 34 | required: true 35 | 36 | - type: input 37 | attributes: 38 | label: "Potential Commit/PR that introduced the regression" 39 | description: "If you have time to investigate, what PR/date/version introduced this issue" 40 | placeholder: "PR #123 or commit 5b3c4a4" 41 | 42 | - type: input 43 | attributes: 44 | label: "Versions" 45 | description: "From which version of `@nestjs/cli` to which version you are upgrading" 46 | placeholder: "8.1.0 -> 8.1.3" 47 | 48 | - type: textarea 49 | validations: 50 | required: true 51 | attributes: 52 | label: "Describe the regression" 53 | description: "A clear and concise description of what the regression is" 54 | 55 | - type: textarea 56 | attributes: 57 | label: "Minimum reproduction code" 58 | description: | 59 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 60 | **Tip:** If you leave a minimum repository, we will understand your issue faster! 61 | value: | 62 | ```ts 63 | 64 | ``` 65 | 66 | - type: textarea 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Expected behavior" 71 | description: "A clear and concise description of what you expected to happend (or code)" 72 | 73 | - type: textarea 74 | attributes: 75 | label: "Other" 76 | description: | 77 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 78 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## To encourage contributors to use issue templates, we don't allow blank issues 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: "\u2753 Discord Community of NestJS" 6 | url: "https://discord.gg/NestJS" 7 | about: "Please ask support questions or discuss suggestions/enhancements here." 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | ``` 14 | [ ] Bugfix 15 | [ ] Feature 16 | [ ] Code style update (formatting, local variables) 17 | [ ] Refactoring (no functional changes, no api changes) 18 | [ ] Build related changes 19 | [ ] CI related changes 20 | [ ] Other... Please describe: 21 | ``` 22 | 23 | ## What is the current behavior? 24 | 25 | 26 | Issue Number: N/A 27 | 28 | 29 | ## What is the new behavior? 30 | 31 | 32 | ## Does this PR introduce a breaking change? 33 | ``` 34 | [ ] Yes 35 | [ ] No 36 | ``` 37 | 38 | 39 | 40 | 41 | ## Other information 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .envrc 3 | node_modules/ 4 | .DS_Store 5 | *.map 6 | 7 | # output 8 | lib/**/*.js 9 | commands/**/*.js 10 | actions/**/*.js 11 | bin/**/*.js 12 | lib/**/*.d.ts 13 | commands/**/*.d.ts 14 | actions/**/*.d.ts 15 | bin/**/*.d.ts 16 | 17 | # configuration 18 | renovate.json 19 | gulpfile.js 20 | /.vscode 21 | 22 | # sample 23 | /sample 24 | 25 | yarn.lock -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .circleci 3 | .github 4 | .gitignore 5 | .prettierrc 6 | .travis.yml 7 | Makefile 8 | node_modules/ 9 | .DS_Store 10 | CONTRIBUTE.md 11 | 12 | # source 13 | **/*.ts 14 | *.ts 15 | /test 16 | 17 | # definitions 18 | !**/*.d.ts 19 | !*.d.ts 20 | 21 | # configuration 22 | renovate.json 23 | gulpfile.js 24 | tsconfig.json 25 | tools 26 | /test/jest-config.json 27 | /.vscode 28 | .commitlintrc.json 29 | .eslintrc.js 30 | .eslintignore 31 | .release-it.json 32 | 33 | # sample 34 | /sample -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "quoteProps": "preserve", 5 | } -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "tagName": "${version}", 4 | "commitMessage": "chore(): release v${version}" 5 | }, 6 | "github": { 7 | "release": true, 8 | "web": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2025 4 | Kamil Mysliwiec 5 | Thomas Ricart 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 |

A progressive Node.js framework for building efficient and scalable server-side applications.

6 |

7 | NPM Version 8 | Package License 9 | NPM Downloads 10 | Discord 11 | Backers on Open Collective 12 | Sponsors on Open Collective 13 | 14 | Support us 15 | 16 | 17 | ## Description 18 | 19 | The Nest CLI is a command-line interface tool that helps you to initialize, develop, and maintain your Nest applications. It assists in multiple ways, including scaffolding the project, serving it in development mode, and building and bundling the application for production distribution. It embodies best-practice architectural patterns to encourage well-structured apps. 20 | 21 | The CLI works with [schematics](https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/schematics), and provides built in support from the schematics collection at [@nestjs/schematics](https://github.com/nestjs/schematics). 22 | 23 | Read more [here](https://docs.nestjs.com/cli/overview). 24 | 25 | ## Installation 26 | 27 | ``` 28 | $ npm install -g @nestjs/cli 29 | ``` 30 | 31 | ## Usage 32 | 33 | Learn more in the [official documentation](https://docs.nestjs.com/cli/overview). 34 | 35 | ## Stay in touch 36 | 37 | - Website - [https://nestjs.com](https://nestjs.com/) 38 | - Twitter - [@nestframework](https://twitter.com/nestframework) 39 | 40 | ## License 41 | 42 | Nest is [MIT licensed](LICENSE). 43 | -------------------------------------------------------------------------------- /actions/abstract.action.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../commands'; 2 | 3 | export abstract class AbstractAction { 4 | public abstract handle( 5 | inputs?: Input[], 6 | options?: Input[], 7 | extraFlags?: string[], 8 | ): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /actions/generate.action.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'ansis'; 2 | import * as path from 'path'; 3 | import { Input } from '../commands'; 4 | import { getValueOrDefault } from '../lib/compiler/helpers/get-value-or-default'; 5 | import { 6 | AbstractCollection, 7 | Collection, 8 | CollectionFactory, 9 | SchematicOption, 10 | } from '../lib/schematics'; 11 | import { MESSAGES } from '../lib/ui'; 12 | import { loadConfiguration } from '../lib/utils/load-configuration'; 13 | import { 14 | askForProjectName, 15 | getSpecFileSuffix, 16 | moveDefaultProjectToStart, 17 | shouldAskForProject, 18 | shouldGenerateFlat, 19 | shouldGenerateSpec, 20 | } from '../lib/utils/project-utils'; 21 | import { AbstractAction } from './abstract.action'; 22 | import { assertNonArray } from '../lib/utils/type-assertions'; 23 | 24 | export class GenerateAction extends AbstractAction { 25 | public async handle(inputs: Input[], options: Input[]) { 26 | await generateFiles(inputs.concat(options)); 27 | } 28 | } 29 | 30 | const generateFiles = async (inputs: Input[]) => { 31 | const configuration = await loadConfiguration(); 32 | const collectionOption = inputs.find( 33 | (option) => option.name === 'collection', 34 | )!.value as string; 35 | const schematic = inputs.find((option) => option.name === 'schematic')! 36 | .value as string; 37 | const appName = inputs.find((option) => option.name === 'project')! 38 | .value as string; 39 | const spec = inputs.find((option) => option.name === 'spec'); 40 | const flat = inputs.find((option) => option.name === 'flat'); 41 | const specFileSuffix = inputs.find( 42 | (option) => option.name === 'specFileSuffix', 43 | ); 44 | 45 | const collection: AbstractCollection = CollectionFactory.create( 46 | collectionOption || configuration.collection || Collection.NESTJS, 47 | ); 48 | const schematicOptions: SchematicOption[] = mapSchematicOptions(inputs); 49 | schematicOptions.push( 50 | new SchematicOption('language', configuration.language), 51 | ); 52 | const configurationProjects = configuration.projects; 53 | 54 | let sourceRoot = appName 55 | ? getValueOrDefault(configuration, 'sourceRoot', appName) 56 | : configuration.sourceRoot; 57 | 58 | const specValue = spec!.value as boolean; 59 | const flatValue = !!flat?.value; 60 | const specFileSuffixValue = specFileSuffix!.value as string; 61 | const specOptions = spec!.options as any; 62 | let generateSpec = shouldGenerateSpec( 63 | configuration, 64 | schematic, 65 | appName, 66 | specValue, 67 | specOptions.passedAsInput, 68 | ); 69 | let generateFlat = shouldGenerateFlat(configuration, appName, flatValue); 70 | let generateSpecFileSuffix = getSpecFileSuffix( 71 | configuration, 72 | appName, 73 | specFileSuffixValue, 74 | ); 75 | 76 | // If you only add a `lib` we actually don't have monorepo: true BUT we do have "projects" 77 | // Ensure we don't run for new app/libs schematics 78 | if (shouldAskForProject(schematic, configurationProjects, appName)) { 79 | const defaultLabel = ' [ Default ]'; 80 | let defaultProjectName: string = configuration.sourceRoot + defaultLabel; 81 | 82 | for (const property in configurationProjects) { 83 | if ( 84 | configurationProjects[property].sourceRoot === configuration.sourceRoot 85 | ) { 86 | defaultProjectName = property + defaultLabel; 87 | break; 88 | } 89 | } 90 | 91 | const projects = moveDefaultProjectToStart( 92 | configuration, 93 | defaultProjectName, 94 | defaultLabel, 95 | ); 96 | 97 | const selectedProjectName = (await askForProjectName( 98 | MESSAGES.PROJECT_SELECTION_QUESTION, 99 | projects, 100 | )) as string; 101 | 102 | const project: string = selectedProjectName.replace(defaultLabel, ''); 103 | if (project !== configuration.sourceRoot) { 104 | sourceRoot = configurationProjects[project].sourceRoot; 105 | } 106 | 107 | if (selectedProjectName !== defaultProjectName) { 108 | // Only overwrite if the appName is not the default- as it has already been loaded above 109 | generateSpec = shouldGenerateSpec( 110 | configuration, 111 | schematic, 112 | selectedProjectName, 113 | specValue, 114 | specOptions.passedAsInput, 115 | ); 116 | generateFlat = shouldGenerateFlat( 117 | configuration, 118 | selectedProjectName, 119 | flatValue, 120 | ); 121 | generateSpecFileSuffix = getSpecFileSuffix( 122 | configuration, 123 | appName, 124 | specFileSuffixValue, 125 | ); 126 | } 127 | } 128 | 129 | if (configuration.generateOptions?.baseDir) { 130 | sourceRoot = path.join(sourceRoot, configuration.generateOptions.baseDir); 131 | } 132 | 133 | schematicOptions.push(new SchematicOption('sourceRoot', sourceRoot)); 134 | schematicOptions.push(new SchematicOption('spec', generateSpec)); 135 | schematicOptions.push(new SchematicOption('flat', generateFlat)); 136 | schematicOptions.push( 137 | new SchematicOption('specFileSuffix', generateSpecFileSuffix), 138 | ); 139 | try { 140 | const schematicInput = inputs.find((input) => input.name === 'schematic'); 141 | if (!schematicInput) { 142 | throw new Error('Unable to find a schematic for this configuration'); 143 | } 144 | await collection.execute(schematicInput.value as string, schematicOptions); 145 | } catch (error) { 146 | if (error && error.message) { 147 | console.error(red(error.message)); 148 | } 149 | } 150 | }; 151 | 152 | const mapSchematicOptions = (inputs: Input[]): SchematicOption[] => { 153 | const excludedInputNames = ['schematic', 'spec', 'flat', 'specFileSuffix']; 154 | const options: SchematicOption[] = []; 155 | inputs.forEach((input) => { 156 | if (!excludedInputNames.includes(input.name) && input.value !== undefined) { 157 | assertNonArray(input.value); 158 | options.push(new SchematicOption(input.name, input.value)); 159 | } 160 | }); 161 | return options; 162 | }; 163 | -------------------------------------------------------------------------------- /actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstract.action'; 2 | export * from './build.action'; 3 | export * from './generate.action'; 4 | export * from './info.action'; 5 | export * from './new.action'; 6 | export * from './start.action'; 7 | export * from './add.action'; 8 | -------------------------------------------------------------------------------- /bin/nest.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as commander from 'commander'; 3 | import { CommanderStatic } from 'commander'; 4 | import { CommandLoader } from '../commands'; 5 | import { 6 | loadLocalBinCommandLoader, 7 | localBinExists, 8 | } from '../lib/utils/local-binaries'; 9 | 10 | const bootstrap = async () => { 11 | const program: CommanderStatic = commander; 12 | program 13 | .version( 14 | require('../package.json').version, 15 | '-v, --version', 16 | 'Output the current version.', 17 | ) 18 | .usage(' [options]') 19 | .helpOption('-h, --help', 'Output usage information.'); 20 | 21 | if (localBinExists()) { 22 | const localCommandLoader = loadLocalBinCommandLoader(); 23 | await localCommandLoader.load(program); 24 | } else { 25 | await CommandLoader.load(program); 26 | } 27 | await commander.parseAsync(process.argv); 28 | 29 | if (!process.argv.slice(2).length) { 30 | program.outputHelp(); 31 | } 32 | }; 33 | 34 | bootstrap(); 35 | -------------------------------------------------------------------------------- /commands/abstract.command.ts: -------------------------------------------------------------------------------- 1 | import { CommanderStatic } from 'commander'; 2 | import { AbstractAction } from '../actions/abstract.action'; 3 | 4 | export abstract class AbstractCommand { 5 | constructor(protected action: AbstractAction) {} 6 | 7 | public abstract load(program: CommanderStatic): void; 8 | } 9 | -------------------------------------------------------------------------------- /commands/add.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommanderStatic } from 'commander'; 2 | import { getRemainingFlags } from '../lib/utils/remaining-flags'; 3 | import { AbstractCommand } from './abstract.command'; 4 | import { Input } from './command.input'; 5 | 6 | export class AddCommand extends AbstractCommand { 7 | public load(program: CommanderStatic): void { 8 | program 9 | .command('add ') 10 | .allowUnknownOption() 11 | .description('Adds support for an external library to your project.') 12 | .option( 13 | '-d, --dry-run', 14 | 'Report actions that would be performed without writing out results.', 15 | ) 16 | .option('-s, --skip-install', 'Skip package installation.', false) 17 | .option('-p, --project [project]', 'Project in which to generate files.') 18 | .usage(' [options] [library-specific-options]') 19 | .action(async (library: string, command: Command) => { 20 | const options: Input[] = []; 21 | options.push({ name: 'dry-run', value: !!command.dryRun }); 22 | options.push({ name: 'skip-install', value: command.skipInstall }); 23 | options.push({ 24 | name: 'project', 25 | value: command.project, 26 | }); 27 | 28 | const inputs: Input[] = []; 29 | inputs.push({ name: 'library', value: library }); 30 | 31 | const flags = getRemainingFlags(program); 32 | try { 33 | await this.action.handle(inputs, options, flags); 34 | } catch (err) { 35 | process.exit(1); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /commands/build.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommanderStatic } from 'commander'; 2 | import { ERROR_PREFIX } from '../lib/ui'; 3 | import { AbstractCommand } from './abstract.command'; 4 | import { Input } from './command.input'; 5 | 6 | export class BuildCommand extends AbstractCommand { 7 | public load(program: CommanderStatic): void { 8 | program 9 | .command('build [apps...]') 10 | .option('-c, --config [path]', 'Path to nest-cli configuration file.') 11 | .option('-p, --path [path]', 'Path to tsconfig file.') 12 | .option('-w, --watch', 'Run in watch mode (live-reload).') 13 | .option('-b, --builder [name]', 'Builder to be used (tsc, webpack, swc).') 14 | .option('--watchAssets', 'Watch non-ts (e.g., .graphql) files mode.') 15 | .option( 16 | '--webpack', 17 | 'Use webpack for compilation (deprecated option, use --builder instead).', 18 | ) 19 | .option('--type-check', 'Enable type checking (when SWC is used).') 20 | .option('--webpackPath [path]', 'Path to webpack configuration.') 21 | .option('--tsc', 'Use typescript compiler for compilation.') 22 | .option( 23 | '--preserveWatchOutput', 24 | 'Use "preserveWatchOutput" option when using tsc watch mode.', 25 | ) 26 | .option('--all', 'Build all projects in a monorepo.') 27 | .description('Build Nest application.') 28 | .action(async (apps: string[], command: Command) => { 29 | const options: Input[] = []; 30 | 31 | options.push({ 32 | name: 'config', 33 | value: command.config, 34 | }); 35 | 36 | const isWebpackEnabled = command.tsc ? false : command.webpack; 37 | options.push({ name: 'webpack', value: isWebpackEnabled }); 38 | options.push({ name: 'watch', value: !!command.watch }); 39 | options.push({ name: 'watchAssets', value: !!command.watchAssets }); 40 | options.push({ 41 | name: 'path', 42 | value: command.path, 43 | }); 44 | options.push({ 45 | name: 'webpackPath', 46 | value: command.webpackPath, 47 | }); 48 | 49 | const availableBuilders = ['tsc', 'webpack', 'swc']; 50 | if (command.builder && !availableBuilders.includes(command.builder)) { 51 | console.error( 52 | ERROR_PREFIX + 53 | ` Invalid builder option: ${ 54 | command.builder 55 | }. Available builders: ${availableBuilders.join(', ')}`, 56 | ); 57 | return; 58 | } 59 | options.push({ 60 | name: 'builder', 61 | value: command.builder, 62 | }); 63 | 64 | options.push({ 65 | name: 'typeCheck', 66 | value: command.typeCheck, 67 | }); 68 | 69 | options.push({ 70 | name: 'preserveWatchOutput', 71 | value: 72 | !!command.preserveWatchOutput && 73 | !!command.watch && 74 | !isWebpackEnabled, 75 | }); 76 | 77 | options.push({ name: 'all', value: !!command.all }); 78 | 79 | const inputs: Input[] = apps.map((app) => ({ 80 | name: 'app', 81 | value: app, 82 | })); 83 | 84 | // Handle the default project for `nest build` with no args 85 | // The action instance will pick up the default project when value is `undefined` 86 | if (inputs.length === 0) { 87 | inputs.push({ name: 'app', value: undefined! }); 88 | } 89 | 90 | await this.action.handle(inputs, options); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /commands/command.input.ts: -------------------------------------------------------------------------------- 1 | export interface Input { 2 | name: string; 3 | value: boolean | string | string[]; 4 | options?: any; 5 | } 6 | -------------------------------------------------------------------------------- /commands/command.loader.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'ansis'; 2 | import { CommanderStatic } from 'commander'; 3 | import { 4 | AddAction, 5 | BuildAction, 6 | GenerateAction, 7 | InfoAction, 8 | NewAction, 9 | StartAction, 10 | } from '../actions'; 11 | import { ERROR_PREFIX } from '../lib/ui'; 12 | import { AddCommand } from './add.command'; 13 | import { BuildCommand } from './build.command'; 14 | import { GenerateCommand } from './generate.command'; 15 | import { InfoCommand } from './info.command'; 16 | import { NewCommand } from './new.command'; 17 | import { StartCommand } from './start.command'; 18 | export class CommandLoader { 19 | public static async load(program: CommanderStatic): Promise { 20 | new NewCommand(new NewAction()).load(program); 21 | new BuildCommand(new BuildAction()).load(program); 22 | new StartCommand(new StartAction()).load(program); 23 | new InfoCommand(new InfoAction()).load(program); 24 | new AddCommand(new AddAction()).load(program); 25 | await new GenerateCommand(new GenerateAction()).load(program); 26 | 27 | this.handleInvalidCommand(program); 28 | } 29 | 30 | private static handleInvalidCommand(program: CommanderStatic) { 31 | program.on('command:*', () => { 32 | console.error( 33 | `\n${ERROR_PREFIX} Invalid command: ${red`%s`}`, 34 | program.args.join(' '), 35 | ); 36 | console.log( 37 | `See ${red`--help`} for a list of available commands.\n`, 38 | ); 39 | process.exit(1); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /commands/generate.command.ts: -------------------------------------------------------------------------------- 1 | import { bold, cyan, green } from 'ansis'; 2 | import * as Table from 'cli-table3'; 3 | import { Command, CommanderStatic } from 'commander'; 4 | import { AbstractCollection, CollectionFactory } from '../lib/schematics'; 5 | import { Schematic } from '../lib/schematics/nest.collection'; 6 | import { loadConfiguration } from '../lib/utils/load-configuration'; 7 | import { AbstractCommand } from './abstract.command'; 8 | import { Input } from './command.input'; 9 | 10 | export class GenerateCommand extends AbstractCommand { 11 | public async load(program: CommanderStatic): Promise { 12 | program 13 | .command('generate [name] [path]') 14 | .alias('g') 15 | .description(await this.buildDescription()) 16 | .option( 17 | '-d, --dry-run', 18 | 'Report actions that would be taken without writing out results.', 19 | ) 20 | .option('-p, --project [project]', 'Project in which to generate files.') 21 | .option( 22 | '--flat', 23 | 'Enforce flat structure of generated element.', 24 | () => true, 25 | ) 26 | .option( 27 | '--no-flat', 28 | 'Enforce that directories are generated.', 29 | () => false, 30 | ) 31 | .option( 32 | '--spec', 33 | 'Enforce spec files generation.', 34 | () => { 35 | return { value: true, passedAsInput: true }; 36 | }, 37 | true, 38 | ) 39 | .option( 40 | '--spec-file-suffix [suffix]', 41 | 'Use a custom suffix for spec files.', 42 | ) 43 | .option('--skip-import', 'Skip importing', () => true, false) 44 | .option('--no-spec', 'Disable spec files generation.', () => { 45 | return { value: false, passedAsInput: true }; 46 | }) 47 | .option( 48 | '-c, --collection [collectionName]', 49 | 'Schematics collection to use.', 50 | ) 51 | .action( 52 | async ( 53 | schematic: string, 54 | name: string, 55 | path: string, 56 | command: Command, 57 | ) => { 58 | const options: Input[] = []; 59 | options.push({ name: 'dry-run', value: !!command.dryRun }); 60 | 61 | if (command.flat !== undefined) { 62 | options.push({ name: 'flat', value: command.flat }); 63 | } 64 | 65 | options.push({ 66 | name: 'spec', 67 | value: 68 | typeof command.spec === 'boolean' 69 | ? command.spec 70 | : command.spec.value, 71 | options: { 72 | passedAsInput: 73 | typeof command.spec === 'boolean' 74 | ? false 75 | : command.spec.passedAsInput, 76 | }, 77 | }); 78 | options.push({ 79 | name: 'specFileSuffix', 80 | value: command.specFileSuffix, 81 | }); 82 | options.push({ 83 | name: 'collection', 84 | value: command.collection, 85 | }); 86 | options.push({ 87 | name: 'project', 88 | value: command.project, 89 | }); 90 | 91 | options.push({ 92 | name: 'skipImport', 93 | value: command.skipImport, 94 | }); 95 | 96 | const inputs: Input[] = []; 97 | inputs.push({ name: 'schematic', value: schematic }); 98 | inputs.push({ name: 'name', value: name }); 99 | inputs.push({ name: 'path', value: path }); 100 | 101 | await this.action.handle(inputs, options); 102 | }, 103 | ); 104 | } 105 | 106 | private async buildDescription(): Promise { 107 | const collection = await this.getCollection(); 108 | return ( 109 | 'Generate a Nest element.\n' + 110 | ` Schematics available on ${bold(collection)} collection:\n` + 111 | this.buildSchematicsListAsTable(await this.getSchematics(collection)) 112 | ); 113 | } 114 | 115 | private buildSchematicsListAsTable(schematics: Schematic[]): string { 116 | const leftMargin = ' '; 117 | const tableConfig = { 118 | head: ['name', 'alias', 'description'], 119 | chars: { 120 | 'left': leftMargin.concat('│'), 121 | 'top-left': leftMargin.concat('┌'), 122 | 'bottom-left': leftMargin.concat('└'), 123 | 'mid': '', 124 | 'left-mid': '', 125 | 'mid-mid': '', 126 | 'right-mid': '', 127 | }, 128 | }; 129 | const table: any = new Table(tableConfig); 130 | for (const schematic of schematics) { 131 | table.push([ 132 | green(schematic.name), 133 | cyan(schematic.alias), 134 | schematic.description, 135 | ]); 136 | } 137 | return table.toString(); 138 | } 139 | 140 | private async getCollection(): Promise { 141 | const configuration = await loadConfiguration(); 142 | return configuration.collection; 143 | } 144 | 145 | private async getSchematics(collection: string): Promise { 146 | const abstractCollection: AbstractCollection = 147 | CollectionFactory.create(collection); 148 | return abstractCollection.getSchematics(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './command.loader'; 2 | export * from './command.input'; 3 | -------------------------------------------------------------------------------- /commands/info.command.ts: -------------------------------------------------------------------------------- 1 | import { CommanderStatic } from 'commander'; 2 | import { AbstractCommand } from './abstract.command'; 3 | 4 | export class InfoCommand extends AbstractCommand { 5 | public load(program: CommanderStatic) { 6 | program 7 | .command('info') 8 | .alias('i') 9 | .description('Display Nest project details.') 10 | .action(async () => { 11 | await this.action.handle(); 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /commands/new.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommanderStatic } from 'commander'; 2 | import { Collection } from '../lib/schematics'; 3 | import { AbstractCommand } from './abstract.command'; 4 | import { Input } from './command.input'; 5 | 6 | export class NewCommand extends AbstractCommand { 7 | public load(program: CommanderStatic) { 8 | program 9 | .command('new [name]') 10 | .alias('n') 11 | .description('Generate Nest application.') 12 | .option('--directory [directory]', 'Specify the destination directory') 13 | .option( 14 | '-d, --dry-run', 15 | 'Report actions that would be performed without writing out results.', 16 | false, 17 | ) 18 | .option('-g, --skip-git', 'Skip git repository initialization.', false) 19 | .option('-s, --skip-install', 'Skip package installation.', false) 20 | .option( 21 | '-p, --package-manager [packageManager]', 22 | 'Specify package manager.', 23 | ) 24 | .option( 25 | '-l, --language [language]', 26 | 'Programming language to be used (TypeScript or JavaScript)', 27 | 'TypeScript', 28 | ) 29 | .option( 30 | '-c, --collection [collectionName]', 31 | 'Schematics collection to use', 32 | Collection.NESTJS, 33 | ) 34 | .option('--strict', 'Enables strict mode in TypeScript.', false) 35 | .action(async (name: string, command: Command) => { 36 | const options: Input[] = []; 37 | const availableLanguages = ['js', 'ts', 'javascript', 'typescript']; 38 | options.push({ name: 'directory', value: command.directory }); 39 | options.push({ name: 'dry-run', value: command.dryRun }); 40 | options.push({ name: 'skip-git', value: command.skipGit }); 41 | options.push({ name: 'skip-install', value: command.skipInstall }); 42 | options.push({ name: 'strict', value: command.strict }); 43 | options.push({ 44 | name: 'packageManager', 45 | value: command.packageManager, 46 | }); 47 | options.push({ name: 'collection', value: command.collection }); 48 | 49 | if (!!command.language) { 50 | const lowercasedLanguage = command.language.toLowerCase(); 51 | const langMatch = availableLanguages.includes(lowercasedLanguage); 52 | if (!langMatch) { 53 | throw new Error( 54 | `Invalid language "${command.language}" selected. Available languages are "typescript" or "javascript"`, 55 | ); 56 | } 57 | switch (lowercasedLanguage) { 58 | case 'javascript': 59 | command.language = 'js'; 60 | break; 61 | case 'typescript': 62 | command.language = 'ts'; 63 | break; 64 | default: 65 | command.language = lowercasedLanguage; 66 | break; 67 | } 68 | } 69 | options.push({ 70 | name: 'language', 71 | value: command.language, 72 | }); 73 | 74 | const inputs: Input[] = []; 75 | inputs.push({ name: 'name', value: name }); 76 | 77 | await this.action.handle(inputs, options); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /commands/start.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommanderStatic } from 'commander'; 2 | import { ERROR_PREFIX } from '../lib/ui'; 3 | import { getRemainingFlags } from '../lib/utils/remaining-flags'; 4 | import { AbstractCommand } from './abstract.command'; 5 | import { Input } from './command.input'; 6 | 7 | export class StartCommand extends AbstractCommand { 8 | public load(program: CommanderStatic): void { 9 | const collect = (value: any, previous: any) => { 10 | return previous.concat([value]); 11 | }; 12 | 13 | program 14 | .command('start [app]') 15 | .allowUnknownOption() 16 | .option('-c, --config [path]', 'Path to nest-cli configuration file.') 17 | .option('-p, --path [path]', 'Path to tsconfig file.') 18 | .option('-w, --watch', 'Run in watch mode (live-reload).') 19 | .option('-b, --builder [name]', 'Builder to be used (tsc, webpack, swc).') 20 | .option('--watchAssets', 'Watch non-ts (e.g., .graphql) files mode.') 21 | .option( 22 | '-d, --debug [hostport] ', 23 | 'Run in debug mode (with --inspect flag).', 24 | ) 25 | .option( 26 | '--webpack', 27 | 'Use webpack for compilation (deprecated option, use --builder instead).', 28 | ) 29 | .option('--webpackPath [path]', 'Path to webpack configuration.') 30 | .option('--type-check', 'Enable type checking (when SWC is used).') 31 | .option('--tsc', 'Use typescript compiler for compilation.') 32 | .option( 33 | '--sourceRoot [sourceRoot]', 34 | 'Points at the root of the source code for the single project in standard mode structures, or the default project in monorepo mode structures.', 35 | ) 36 | .option( 37 | '--entryFile [entryFile]', 38 | "Path to the entry file where this command will work with. Defaults to the one defined at your Nest's CLI config file.", 39 | ) 40 | .option('-e, --exec [binary]', 'Binary to run (default: "node").') 41 | .option( 42 | '--preserveWatchOutput', 43 | 'Use "preserveWatchOutput" option when using tsc watch mode.', 44 | ) 45 | .option( 46 | '--shell', 47 | "Spawn child processes within a shell (see node's child_process.spawn() method docs). Default: true.", 48 | true, 49 | ) 50 | .option('--no-shell', 'Do not spawn child processes within a shell.') 51 | .option( 52 | '--env-file [path]', 53 | 'Path to an env file (.env) to be loaded into the environment.', 54 | collect, 55 | [], 56 | ) 57 | .description('Run Nest application.') 58 | .action(async (app: string, command: Command) => { 59 | const options: Input[] = []; 60 | 61 | options.push({ 62 | name: 'config', 63 | value: command.config, 64 | }); 65 | 66 | const isWebpackEnabled = command.tsc ? false : command.webpack; 67 | options.push({ name: 'webpack', value: isWebpackEnabled }); 68 | options.push({ name: 'debug', value: command.debug }); 69 | options.push({ name: 'watch', value: !!command.watch }); 70 | options.push({ name: 'watchAssets', value: !!command.watchAssets }); 71 | options.push({ 72 | name: 'path', 73 | value: command.path, 74 | }); 75 | options.push({ 76 | name: 'webpackPath', 77 | value: command.webpackPath, 78 | }); 79 | options.push({ 80 | name: 'exec', 81 | value: command.exec, 82 | }); 83 | options.push({ 84 | name: 'sourceRoot', 85 | value: command.sourceRoot, 86 | }); 87 | options.push({ 88 | name: 'entryFile', 89 | value: command.entryFile, 90 | }); 91 | options.push({ 92 | name: 'preserveWatchOutput', 93 | value: 94 | !!command.preserveWatchOutput && 95 | !!command.watch && 96 | !isWebpackEnabled, 97 | }); 98 | options.push({ 99 | name: 'shell', 100 | value: !!command.shell, 101 | }); 102 | options.push({ 103 | name: 'envFile', 104 | value: command.envFile, 105 | }); 106 | 107 | const availableBuilders = ['tsc', 'webpack', 'swc']; 108 | if (command.builder && !availableBuilders.includes(command.builder)) { 109 | console.error( 110 | ERROR_PREFIX + 111 | ` Invalid builder option: ${ 112 | command.builder 113 | }. Available builders: ${availableBuilders.join(', ')}`, 114 | ); 115 | return; 116 | } 117 | options.push({ 118 | name: 'builder', 119 | value: command.builder, 120 | }); 121 | 122 | options.push({ 123 | name: 'typeCheck', 124 | value: command.typeCheck, 125 | }); 126 | 127 | const inputs: Input[] = []; 128 | inputs.push({ name: 'app', value: app }); 129 | const flags = getRemainingFlags(program); 130 | 131 | try { 132 | await this.action.handle(inputs, options, flags); 133 | } catch (err) { 134 | process.exit(1); 135 | } 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all 4 | * the tasks. The tasks are really inside tools/gulp/tasks. 5 | */ 6 | 7 | const path = require('path'); 8 | 9 | const projectDir = __dirname; 10 | const tsconfigPath = path.join(projectDir, 'tools/gulp/tsconfig.json'); 11 | 12 | require('ts-node').register({ 13 | project: tsconfigPath 14 | }); 15 | 16 | require('./tools/gulp/gulpfile'); -------------------------------------------------------------------------------- /lib/compiler/assets-manager.ts: -------------------------------------------------------------------------------- 1 | import * as chokidar from 'chokidar'; 2 | import { copyFileSync, mkdirSync, rmSync, statSync } from 'fs'; 3 | import { sync } from 'glob'; 4 | import { dirname, join, sep } from 'path'; 5 | import { 6 | ActionOnFile, 7 | Asset, 8 | AssetEntry, 9 | Configuration, 10 | } from '../configuration'; 11 | import { copyPathResolve } from './helpers/copy-path-resolve'; 12 | import { getValueOrDefault } from './helpers/get-value-or-default'; 13 | 14 | export class AssetsManager { 15 | private watchAssetsKeyValue: { [key: string]: boolean } = {}; 16 | private watchers: chokidar.FSWatcher[] = []; 17 | private actionInProgress = false; 18 | 19 | /** 20 | * Using on `nest build` to close file watch or the build process will not end 21 | * Interval like process 22 | * If no action has been taken recently close watchers 23 | * If action has been taken recently flag and try again 24 | */ 25 | public closeWatchers() { 26 | // Consider adjusting this for larger files 27 | const timeoutMs = 500; 28 | const closeFn = () => { 29 | if (this.actionInProgress) { 30 | this.actionInProgress = false; 31 | setTimeout(closeFn, timeoutMs); 32 | } else { 33 | this.watchers.forEach((watcher) => watcher.close()); 34 | } 35 | }; 36 | 37 | setTimeout(closeFn, timeoutMs); 38 | } 39 | 40 | public copyAssets( 41 | configuration: Required, 42 | appName: string | undefined, 43 | outDir: string, 44 | watchAssetsMode: boolean, 45 | ) { 46 | const assets = 47 | getValueOrDefault( 48 | configuration, 49 | 'compilerOptions.assets', 50 | appName, 51 | ) || []; 52 | 53 | if (assets.length <= 0) { 54 | return; 55 | } 56 | 57 | try { 58 | let sourceRoot = getValueOrDefault(configuration, 'sourceRoot', appName); 59 | sourceRoot = join(process.cwd(), sourceRoot); 60 | 61 | const filesToCopy = assets.map((item) => { 62 | let includePath = typeof item === 'string' ? item : item.include!; 63 | let excludePath = 64 | typeof item !== 'string' && item.exclude ? item.exclude : undefined; 65 | 66 | includePath = join(sourceRoot, includePath).replace(/\\/g, '/'); 67 | excludePath = excludePath 68 | ? join(sourceRoot, excludePath).replace(/\\/g, '/') 69 | : undefined; 70 | 71 | return { 72 | outDir: typeof item !== 'string' ? item.outDir || outDir : outDir, 73 | glob: includePath, 74 | exclude: excludePath, 75 | flat: typeof item !== 'string' ? item.flat : undefined, // deprecated field 76 | watchAssets: typeof item !== 'string' ? item.watchAssets : undefined, 77 | }; 78 | }); 79 | 80 | const isWatchEnabled = 81 | getValueOrDefault( 82 | configuration, 83 | 'compilerOptions.watchAssets', 84 | appName, 85 | ) || watchAssetsMode; 86 | 87 | for (const item of filesToCopy) { 88 | const option: ActionOnFile = { 89 | action: 'change', 90 | item, 91 | path: '', 92 | sourceRoot, 93 | watchAssetsMode: isWatchEnabled, 94 | }; 95 | 96 | if (isWatchEnabled || item.watchAssets) { 97 | // prettier-ignore 98 | const watcher = chokidar 99 | .watch(sync(item.glob, { 100 | ignore: item.exclude, 101 | dot: true, 102 | })) 103 | .on('add', (path: string) => this.actionOnFile({ ...option, path, action: 'change' })) 104 | .on('change', (path: string) => this.actionOnFile({ ...option, path, action: 'change' })) 105 | .on('unlink', (path: string) => this.actionOnFile({ ...option, path, action: 'unlink' })); 106 | 107 | this.watchers.push(watcher); 108 | } else { 109 | const matchedPaths = sync(item.glob, { 110 | ignore: item.exclude, 111 | dot: true, 112 | }); 113 | const files = item.glob.endsWith('*') 114 | ? matchedPaths.filter((matched) => statSync(matched).isFile()) 115 | : matchedPaths.flatMap((matched) => { 116 | if (statSync(matched).isDirectory()) { 117 | return sync(`${matched}/**/*`, { 118 | ignore: item.exclude, 119 | }).filter((file) => statSync(file).isFile()); 120 | } 121 | return matched; 122 | }); 123 | 124 | for (const path of files) { 125 | this.actionOnFile({ ...option, path, action: 'change' }); 126 | } 127 | } 128 | } 129 | } catch (err) { 130 | throw new Error( 131 | `An error occurred during the assets copying process. ${err.message}`, 132 | ); 133 | } 134 | } 135 | 136 | private actionOnFile(option: ActionOnFile) { 137 | const { action, item, path, sourceRoot, watchAssetsMode } = option; 138 | const isWatchEnabled = watchAssetsMode || item.watchAssets; 139 | 140 | const assetCheckKey = path + (item.outDir ?? ''); 141 | // Allow to do action for the first time before check watchMode 142 | if (!isWatchEnabled && this.watchAssetsKeyValue[assetCheckKey]) { 143 | return; 144 | } 145 | // Set path value to true for watching the first time 146 | this.watchAssetsKeyValue[assetCheckKey] = true; 147 | // Set action to true to avoid watches getting cutoff 148 | this.actionInProgress = true; 149 | 150 | const dest = copyPathResolve( 151 | path, 152 | item.outDir!, 153 | sourceRoot.split(sep).length, 154 | ); 155 | 156 | // Copy to output dir if file is changed or added 157 | if (action === 'change') { 158 | mkdirSync(dirname(dest), { recursive: true }); 159 | copyFileSync(path, dest); 160 | } else if (action === 'unlink') { 161 | // Remove from output dir if file is deleted 162 | rmSync(dest, { force: true }); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/compiler/base-compiler.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join, normalize, relative } from 'path'; 2 | import { Configuration } from '../configuration'; 3 | import { getValueOrDefault } from './helpers/get-value-or-default'; 4 | import { PluginsLoader } from './plugins/plugins-loader'; 5 | 6 | export abstract class BaseCompiler> { 7 | constructor(private readonly pluginsLoader: PluginsLoader) {} 8 | 9 | public abstract run( 10 | configuration: Required, 11 | tsConfigPath: string, 12 | appName: string | undefined, 13 | extras?: T, 14 | onSuccess?: () => void, 15 | ): any; 16 | 17 | public loadPlugins( 18 | configuration: Required, 19 | tsConfigPath: string, 20 | appName: string | undefined, 21 | ) { 22 | const pluginsConfig = getValueOrDefault( 23 | configuration, 24 | 'compilerOptions.plugins', 25 | appName, 26 | ); 27 | const pathToSource = this.getPathToSource( 28 | configuration, 29 | tsConfigPath, 30 | appName, 31 | ); 32 | 33 | const plugins = this.pluginsLoader.load(pluginsConfig, { pathToSource }); 34 | return plugins; 35 | } 36 | 37 | public getPathToSource( 38 | configuration: Required, 39 | tsConfigPath: string, 40 | appName: string | undefined, 41 | ) { 42 | const sourceRoot = getValueOrDefault( 43 | configuration, 44 | 'sourceRoot', 45 | appName, 46 | 'sourceRoot', 47 | ); 48 | const cwd = process.cwd(); 49 | const relativeRootPath = dirname(relative(cwd, tsConfigPath)); 50 | const pathToSource = 51 | normalize(sourceRoot).indexOf(normalize(relativeRootPath)) >= 0 52 | ? join(cwd, sourceRoot) 53 | : join(cwd, relativeRootPath, sourceRoot); 54 | 55 | return pathToSource; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/compiler/compiler.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Configuration } from '../configuration'; 3 | import { BaseCompiler } from './base-compiler'; 4 | import { TsConfigProvider } from './helpers/tsconfig-provider'; 5 | import { tsconfigPathsBeforeHookFactory } from './hooks/tsconfig-paths.hook'; 6 | import { PluginsLoader } from './plugins/plugins-loader'; 7 | import { TypeScriptBinaryLoader } from './typescript-loader'; 8 | 9 | export class Compiler extends BaseCompiler { 10 | constructor( 11 | pluginsLoader: PluginsLoader, 12 | private readonly tsConfigProvider: TsConfigProvider, 13 | private readonly typescriptLoader: TypeScriptBinaryLoader, 14 | ) { 15 | super(pluginsLoader); 16 | } 17 | 18 | public run( 19 | configuration: Required, 20 | tsConfigPath: string, 21 | appName: string | undefined, 22 | _extras: any, 23 | onSuccess?: () => void, 24 | ) { 25 | const tsBinary = this.typescriptLoader.load(); 26 | const formatHost: ts.FormatDiagnosticsHost = { 27 | getCanonicalFileName: (path) => path, 28 | getCurrentDirectory: tsBinary.sys.getCurrentDirectory, 29 | getNewLine: () => tsBinary.sys.newLine, 30 | }; 31 | 32 | const { options, fileNames, projectReferences } = 33 | this.tsConfigProvider.getByConfigFilename(tsConfigPath); 34 | 35 | const createProgram = 36 | tsBinary.createIncrementalProgram || tsBinary.createProgram; 37 | const program = createProgram.call(ts, { 38 | rootNames: fileNames, 39 | projectReferences, 40 | options, 41 | }); 42 | 43 | const plugins = this.loadPlugins(configuration, tsConfigPath, appName); 44 | const tsconfigPathsPlugin = tsconfigPathsBeforeHookFactory(options); 45 | const programRef = program.getProgram 46 | ? program.getProgram() 47 | : (program as any as ts.Program); 48 | 49 | const before = plugins.beforeHooks.map((hook) => hook(programRef)); 50 | const after = plugins.afterHooks.map((hook) => hook(programRef)); 51 | const afterDeclarations = plugins.afterDeclarationsHooks.map((hook) => 52 | hook(programRef), 53 | ); 54 | 55 | const emitResult = program.emit( 56 | undefined, 57 | undefined, 58 | undefined, 59 | undefined, 60 | { 61 | before: tsconfigPathsPlugin 62 | ? before.concat(tsconfigPathsPlugin) 63 | : before, 64 | after, 65 | afterDeclarations, 66 | }, 67 | ); 68 | 69 | const errorsCount = this.reportAfterCompilationDiagnostic( 70 | program as any, 71 | emitResult, 72 | tsBinary, 73 | formatHost, 74 | ); 75 | if (errorsCount) { 76 | process.exit(1); 77 | } else if (!errorsCount && onSuccess) { 78 | onSuccess(); 79 | } 80 | } 81 | 82 | private reportAfterCompilationDiagnostic( 83 | program: ts.EmitAndSemanticDiagnosticsBuilderProgram, 84 | emitResult: ts.EmitResult, 85 | tsBinary: typeof ts, 86 | formatHost: ts.FormatDiagnosticsHost, 87 | ): number { 88 | const diagnostics = tsBinary 89 | .getPreEmitDiagnostics(program as unknown as ts.Program) 90 | .concat(emitResult.diagnostics); 91 | 92 | if (diagnostics.length > 0) { 93 | console.error( 94 | tsBinary.formatDiagnosticsWithColorAndContext(diagnostics, formatHost), 95 | ); 96 | console.info( 97 | `Found ${diagnostics.length} error(s).` + tsBinary.sys.newLine, 98 | ); 99 | } 100 | return diagnostics.length; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/compiler/defaults/swc-defaults.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Configuration } from '../../configuration'; 3 | 4 | export const swcDefaultsFactory = ( 5 | tsOptions?: ts.CompilerOptions, 6 | configuration?: Configuration, 7 | ) => { 8 | const builderOptions = 9 | typeof configuration?.compilerOptions?.builder !== 'string' 10 | ? configuration?.compilerOptions?.builder?.options 11 | : {}; 12 | 13 | return { 14 | swcOptions: { 15 | sourceMaps: 16 | tsOptions?.sourceMap || (tsOptions?.inlineSourceMap && 'inline'), 17 | module: { 18 | type: 'commonjs', 19 | }, 20 | jsc: { 21 | target: 'es2021', 22 | parser: { 23 | syntax: 'typescript', 24 | decorators: true, 25 | dynamicImport: true, 26 | }, 27 | transform: { 28 | legacyDecorator: true, 29 | decoratorMetadata: true, 30 | useDefineForClassFields: false, 31 | }, 32 | keepClassNames: true, 33 | baseUrl: tsOptions?.baseUrl, 34 | paths: tsOptions?.paths, 35 | }, 36 | minify: false, 37 | swcrc: true, 38 | }, 39 | cliOptions: { 40 | outDir: tsOptions?.outDir ? convertPath(tsOptions.outDir) : 'dist', 41 | filenames: [configuration?.sourceRoot ?? 'src'], 42 | sync: false, 43 | extensions: ['.js', '.ts'], 44 | copyFiles: false, 45 | includeDotfiles: false, 46 | quiet: false, 47 | watch: false, 48 | stripLeadingPaths: true, 49 | ...builderOptions, 50 | }, 51 | }; 52 | }; 53 | 54 | /** 55 | * Converts Windows specific file paths to posix 56 | * @param windowsPath 57 | */ 58 | function convertPath(windowsPath: string) { 59 | return windowsPath 60 | .replace(/^\\\\\?\\/, '') 61 | .replace(/\\/g, '/') 62 | .replace(/\/\/+/g, '/'); 63 | } 64 | -------------------------------------------------------------------------------- /lib/compiler/defaults/webpack-defaults.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; 3 | import { defaultTsconfigFilename } from '../../configuration/defaults'; 4 | import { appendTsExtension } from '../helpers/append-extension'; 5 | import { MultiNestCompilerPlugins } from '../plugins/plugins-loader'; 6 | import webpack = require('webpack'); 7 | import nodeExternals = require('webpack-node-externals'); 8 | 9 | export const webpackDefaultsFactory = ( 10 | sourceRoot: string, 11 | relativeSourceRoot: string, 12 | entryFilename: string, 13 | isDebugEnabled = false, 14 | tsConfigFile = defaultTsconfigFilename, 15 | plugins: MultiNestCompilerPlugins, 16 | ): webpack.Configuration => { 17 | const isPluginRegistered = isAnyPluginRegistered(plugins); 18 | const webpackConfiguration: webpack.Configuration = { 19 | entry: appendTsExtension(join(sourceRoot, entryFilename)), 20 | devtool: isDebugEnabled ? 'inline-source-map' : false, 21 | target: 'node', 22 | output: { 23 | filename: join(relativeSourceRoot, `${entryFilename}.js`), 24 | }, 25 | ignoreWarnings: [/^(?!CriticalDependenciesWarning$)/], 26 | externals: [nodeExternals() as any], 27 | externalsPresets: { node: true }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /.tsx?$/, 32 | use: [ 33 | { 34 | loader: 'ts-loader', 35 | options: { 36 | transpileOnly: !isPluginRegistered, 37 | configFile: tsConfigFile, 38 | getCustomTransformers: (program: any) => ({ 39 | before: plugins.beforeHooks.map((hook: any) => hook(program)), 40 | after: plugins.afterHooks.map((hook: any) => hook(program)), 41 | afterDeclarations: plugins.afterDeclarationsHooks.map( 42 | (hook: any) => hook(program), 43 | ), 44 | }), 45 | }, 46 | }, 47 | ], 48 | exclude: /node_modules/, 49 | }, 50 | ], 51 | }, 52 | resolve: { 53 | extensions: ['.tsx', '.ts', '.js'], 54 | plugins: [ 55 | new TsconfigPathsPlugin({ 56 | configFile: tsConfigFile, 57 | }), 58 | ], 59 | }, 60 | mode: 'none', 61 | optimization: { 62 | nodeEnv: false, 63 | }, 64 | node: { 65 | __filename: false, 66 | __dirname: false, 67 | }, 68 | plugins: [ 69 | new webpack.IgnorePlugin({ 70 | checkResource(resource: any) { 71 | const lazyImports = [ 72 | '@nestjs/microservices', 73 | '@nestjs/microservices/microservices-module', 74 | '@nestjs/websockets/socket-module', 75 | 'class-validator', 76 | 'class-transformer', 77 | 'class-transformer/storage', 78 | ]; 79 | if (!lazyImports.includes(resource)) { 80 | return false; 81 | } 82 | try { 83 | require.resolve(resource, { 84 | paths: [process.cwd()], 85 | }); 86 | } catch (err) { 87 | return true; 88 | } 89 | return false; 90 | }, 91 | }), 92 | ], 93 | }; 94 | 95 | if (!isPluginRegistered) { 96 | // eslint-disable-next-line @typescript-eslint/no-var-requires 97 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 98 | 99 | webpackConfiguration.plugins!.push( 100 | new ForkTsCheckerWebpackPlugin({ 101 | typescript: { 102 | configFile: tsConfigFile, 103 | }, 104 | }), 105 | ); 106 | } 107 | 108 | return webpackConfiguration; 109 | }; 110 | 111 | function isAnyPluginRegistered(plugins: MultiNestCompilerPlugins) { 112 | return ( 113 | (plugins.afterHooks && plugins.afterHooks.length > 0) || 114 | (plugins.beforeHooks && plugins.beforeHooks.length > 0) || 115 | (plugins.afterDeclarationsHooks && 116 | plugins.afterDeclarationsHooks.length > 0) 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /lib/compiler/helpers/append-extension.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | 3 | export function appendTsExtension(path: string): string { 4 | return extname(path) === '.ts' ? path : path + '.ts'; 5 | } 6 | -------------------------------------------------------------------------------- /lib/compiler/helpers/copy-path-resolve.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | /** 4 | * Helper function for returning a copy destination filename 5 | * 6 | * @description used in `assets-manager.ts` (copy from `copyfiles`) 7 | * @see https://github.com/calvinmetcalf/copyfiles/blob/master/index.js#L22 8 | */ 9 | export function copyPathResolve(filePath: string, outDir: string, up: number) { 10 | return path.join(outDir, dealWith(filePath, up)); 11 | } 12 | 13 | function dealWith(inPath: string, up: number) { 14 | if (!up) { 15 | return inPath; 16 | } 17 | 18 | if (depth(inPath) < up - 1) { 19 | throw new Error('Path outside of project folder is not allowed'); 20 | } 21 | 22 | return path.join(...path.normalize(inPath).split(path.sep).slice(up)); 23 | } 24 | 25 | function depth(string: string) { 26 | return path.normalize(string).split(path.sep).length - 1; 27 | } 28 | -------------------------------------------------------------------------------- /lib/compiler/helpers/delete-out-dir.ts: -------------------------------------------------------------------------------- 1 | import { rm } from 'fs/promises'; 2 | import { Configuration } from '../../configuration'; 3 | import { getValueOrDefault } from './get-value-or-default'; 4 | 5 | export async function deleteOutDirIfEnabled( 6 | configuration: Required, 7 | appName: string | undefined, 8 | dirPath: string, 9 | ) { 10 | const isDeleteEnabled = getValueOrDefault( 11 | configuration, 12 | 'compilerOptions.deleteOutDir', 13 | appName, 14 | ); 15 | if (!isDeleteEnabled) { 16 | return; 17 | } 18 | await rm(dirPath, { recursive: true, force: true }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/compiler/helpers/get-builder.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../commands'; 2 | import { Builder, Configuration } from '../../configuration'; 3 | import { getValueOrDefault } from './get-value-or-default'; 4 | 5 | /** 6 | * Returns the builder to use for the given application. 7 | * @param configuration Configuration object. 8 | * @param cmdOptions Command line options. 9 | * @param appName Application name. 10 | * @returns The builder to use. 11 | */ 12 | export function getBuilder( 13 | configuration: Required, 14 | cmdOptions: Input[], 15 | appName: string | undefined, 16 | ) { 17 | const builderValue = getValueOrDefault( 18 | configuration, 19 | 'compilerOptions.builder', 20 | appName, 21 | 'builder', 22 | cmdOptions, 23 | 'tsc', 24 | ); 25 | return typeof builderValue === 'string' 26 | ? { 27 | type: builderValue, 28 | } 29 | : builderValue; 30 | } 31 | -------------------------------------------------------------------------------- /lib/compiler/helpers/get-tsc-config.path.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../commands'; 2 | import { Builder, Configuration } from '../../configuration'; 3 | import { getDefaultTsconfigPath } from '../../utils/get-default-tsconfig-path'; 4 | import { getValueOrDefault } from './get-value-or-default'; 5 | 6 | /** 7 | * Returns the path to the tsc configuration file to use for the given application. 8 | * @param configuration Configuration object. 9 | * @param cmdOptions Command line options. 10 | * @param appName Application name. 11 | * @returns The path to the tsc configuration file to use. 12 | */ 13 | export function getTscConfigPath( 14 | configuration: Required, 15 | cmdOptions: Input[], 16 | appName: string | undefined, 17 | ) { 18 | let tsconfigPath = getValueOrDefault( 19 | configuration, 20 | 'compilerOptions.tsConfigPath', 21 | appName, 22 | 'path', 23 | cmdOptions, 24 | ); 25 | if (tsconfigPath) { 26 | return tsconfigPath; 27 | } 28 | 29 | const builder = getValueOrDefault( 30 | configuration, 31 | 'compilerOptions.builder', 32 | appName, 33 | ); 34 | 35 | tsconfigPath = 36 | typeof builder === 'object' && builder?.type === 'tsc' 37 | ? builder.options?.configPath 38 | : undefined; 39 | 40 | return tsconfigPath ?? getDefaultTsconfigPath(); 41 | } 42 | -------------------------------------------------------------------------------- /lib/compiler/helpers/get-value-or-default.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../commands'; 2 | import { Configuration } from '../../configuration'; 3 | 4 | export function getValueOrDefault( 5 | configuration: Required, 6 | propertyPath: string, 7 | appName: string | undefined, 8 | key?: 9 | | 'path' 10 | | 'webpack' 11 | | 'webpackPath' 12 | | 'entryFile' 13 | | 'sourceRoot' 14 | | 'exec' 15 | | 'builder' 16 | | 'typeCheck', 17 | options: Input[] = [], 18 | defaultValue?: T, 19 | ): T { 20 | const item = options.find((option) => option.name === key); 21 | const origValue = item && (item.value as unknown as T); 22 | if (origValue !== undefined && origValue !== null) { 23 | return origValue as T; 24 | } 25 | if (configuration.projects && configuration.projects[appName as string]) { 26 | // Wrap the application name in double-quotes to prevent splitting it 27 | // into separate chunks 28 | appName = appName && !appName.includes('"') ? `"${appName}"` : appName; 29 | 30 | const perAppValue = getValueOfPath( 31 | configuration, 32 | `projects.${appName}.`.concat(propertyPath), 33 | ); 34 | if (perAppValue !== undefined) { 35 | return perAppValue as T; 36 | } 37 | } 38 | let value = getValueOfPath(configuration, propertyPath); 39 | if (value === undefined) { 40 | value = defaultValue; 41 | } 42 | return value; 43 | } 44 | 45 | export function getValueOfPath( 46 | object: Record, 47 | propertyPath: string, 48 | ): T { 49 | const fragments = propertyPath.split('.'); 50 | 51 | let propertyValue = object; 52 | let isConcatInProgress = false; 53 | let path = ''; 54 | 55 | for (const fragment of fragments) { 56 | if (!propertyValue) { 57 | break; 58 | } 59 | /** 60 | * When path is escaped with "" double quotes, 61 | * concatenate the property path. 62 | * Reference: https://github.com/nestjs/nest-cli/issues/947 63 | */ 64 | if (fragment.startsWith('"') && fragment.endsWith('"')) { 65 | path = stripDoubleQuotes(fragment); 66 | } else if (fragment.startsWith('"')) { 67 | path += stripDoubleQuotes(fragment) + '.'; 68 | isConcatInProgress = true; 69 | continue; 70 | } else if (isConcatInProgress && !fragment.endsWith('"')) { 71 | path += fragment + '.'; 72 | continue; 73 | } else if (fragment.endsWith('"')) { 74 | path += stripDoubleQuotes(fragment); 75 | isConcatInProgress = false; 76 | } else { 77 | path = fragment; 78 | } 79 | propertyValue = propertyValue[path]; 80 | path = ''; 81 | } 82 | return propertyValue as T; 83 | } 84 | 85 | function stripDoubleQuotes(text: string) { 86 | return text.replace(/"/g, ''); 87 | } 88 | -------------------------------------------------------------------------------- /lib/compiler/helpers/get-webpack-config-path.ts: -------------------------------------------------------------------------------- 1 | import { Input } from '../../../commands'; 2 | import { Builder, Configuration } from '../../configuration'; 3 | import { getValueOrDefault } from './get-value-or-default'; 4 | 5 | /** 6 | * Returns the path to the webpack configuration file to use for the given application. 7 | * @param configuration Configuration object. 8 | * @param cmdOptions Command line options. 9 | * @param appName Application name. 10 | * @returns The path to the webpack configuration file to use. 11 | */ 12 | export function getWebpackConfigPath( 13 | configuration: Required, 14 | cmdOptions: Input[], 15 | appName: string | undefined, 16 | ) { 17 | let webpackPath = getValueOrDefault( 18 | configuration, 19 | 'compilerOptions.webpackConfigPath', 20 | appName, 21 | 'webpackPath', 22 | cmdOptions, 23 | ); 24 | if (webpackPath) { 25 | return webpackPath; 26 | } 27 | 28 | const builder = getValueOrDefault( 29 | configuration, 30 | 'compilerOptions.builder', 31 | appName, 32 | ); 33 | 34 | webpackPath = 35 | typeof builder === 'object' && builder?.type === 'webpack' 36 | ? builder.options?.configPath 37 | : undefined; 38 | return webpackPath; 39 | } 40 | -------------------------------------------------------------------------------- /lib/compiler/helpers/manual-restart.ts: -------------------------------------------------------------------------------- 1 | import { gray } from 'ansis'; 2 | 3 | export function listenForManualRestart(callback: () => void) { 4 | const stdinListener = (data: Buffer) => { 5 | if (data.toString().trim() === 'rs') { 6 | process.stdin.removeListener('data', stdinListener); 7 | callback(); 8 | } 9 | }; 10 | process.stdin.on('data', stdinListener); 11 | } 12 | 13 | export function displayManualRestartTip(): void { 14 | console.log(`To restart at any time, enter ${gray`rs`}.\n`); 15 | } 16 | -------------------------------------------------------------------------------- /lib/compiler/helpers/tsconfig-provider.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { CLI_ERRORS } from '../../ui'; 5 | import { TypeScriptBinaryLoader } from '../typescript-loader'; 6 | 7 | export class TsConfigProvider { 8 | constructor(private readonly typescriptLoader: TypeScriptBinaryLoader) {} 9 | 10 | public getByConfigFilename(configFilename: string) { 11 | const configPath = join(process.cwd(), configFilename); 12 | if (!existsSync(configPath)) { 13 | throw new Error(CLI_ERRORS.MISSING_TYPESCRIPT(configFilename)); 14 | } 15 | const tsBinary = this.typescriptLoader.load(); 16 | const parsedCmd = tsBinary.getParsedCommandLineOfConfigFile( 17 | configPath, 18 | undefined!, 19 | tsBinary.sys as unknown as ts.ParseConfigFileHost, 20 | ); 21 | const { options, fileNames, projectReferences } = parsedCmd!; 22 | return { options, fileNames, projectReferences }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/compiler/hooks/tsconfig-paths.hook.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import { dirname, posix } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { TypeScriptBinaryLoader } from '../typescript-loader'; 5 | import tsPaths = require('tsconfig-paths'); 6 | 7 | export function tsconfigPathsBeforeHookFactory( 8 | compilerOptions: ts.CompilerOptions, 9 | ) { 10 | const tsBinary = new TypeScriptBinaryLoader().load(); 11 | const { paths = {}, baseUrl = './' } = compilerOptions; 12 | const matcher = tsPaths.createMatchPath(baseUrl!, paths, ['main']); 13 | 14 | return (ctx: ts.TransformationContext): ts.Transformer => { 15 | return (sf: ts.SourceFile) => { 16 | const visitNode = (node: ts.Node): ts.Node => { 17 | if ( 18 | tsBinary.isImportDeclaration(node) || 19 | (tsBinary.isExportDeclaration(node) && node.moduleSpecifier) 20 | ) { 21 | try { 22 | const importPathWithQuotes = 23 | node.moduleSpecifier && node.moduleSpecifier.getText(); 24 | 25 | if (!importPathWithQuotes) { 26 | return node; 27 | } 28 | const text = importPathWithQuotes.substring( 29 | 1, 30 | importPathWithQuotes.length - 1, 31 | ); 32 | const result = getNotAliasedPath(sf, matcher, text); 33 | if (!result) { 34 | return node; 35 | } 36 | const moduleSpecifier = 37 | tsBinary.factory.createStringLiteral(result); 38 | (moduleSpecifier as any).parent = ( 39 | node as any 40 | ).moduleSpecifier.parent; 41 | 42 | if (tsBinary.isImportDeclaration(node)) { 43 | const updatedNode = tsBinary.factory.updateImportDeclaration( 44 | node, 45 | node.modifiers, 46 | node.importClause, 47 | moduleSpecifier, 48 | node.assertClause, 49 | ); 50 | (updatedNode as any).flags = node.flags; 51 | return updatedNode; 52 | } else { 53 | const updatedNode = tsBinary.factory.updateExportDeclaration( 54 | node, 55 | node.modifiers, 56 | node.isTypeOnly, 57 | node.exportClause, 58 | moduleSpecifier, 59 | node.assertClause, 60 | ); 61 | (updatedNode as any).flags = node.flags; 62 | return updatedNode; 63 | } 64 | } catch { 65 | return node; 66 | } 67 | } 68 | return tsBinary.visitEachChild(node, visitNode, ctx); 69 | }; 70 | return tsBinary.visitNode(sf, visitNode); 71 | }; 72 | }; 73 | } 74 | 75 | function getNotAliasedPath( 76 | sf: ts.SourceFile, 77 | matcher: tsPaths.MatchPath, 78 | text: string, 79 | ) { 80 | let result = matcher(text, undefined, undefined, [ 81 | '.ts', 82 | '.tsx', 83 | '.js', 84 | '.jsx', 85 | ]); 86 | if (!result) { 87 | return; 88 | } 89 | if (os.platform() === 'win32') { 90 | result = result.replace(/\\/g, '/'); 91 | } 92 | try { 93 | // Installed packages (node modules) should take precedence over root files with the same name. 94 | // Ref: https://github.com/nestjs/nest-cli/issues/838 95 | const packagePath = require.resolve(text, { 96 | paths: [process.cwd(), ...module.paths], 97 | }); 98 | if (packagePath) { 99 | return text; 100 | } 101 | } catch {} 102 | 103 | const resolvedPath = posix.relative(dirname(sf.fileName), result) || './'; 104 | return resolvedPath[0] === '.' ? resolvedPath : './' + resolvedPath; 105 | } 106 | -------------------------------------------------------------------------------- /lib/compiler/interfaces/readonly-visitor.interface.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export type DeepPluginMeta = 4 | | ts.ObjectLiteralExpression 5 | | { 6 | [key: string]: DeepPluginMeta; 7 | }; 8 | 9 | export interface ReadonlyVisitor { 10 | key: string; 11 | typeImports: Record; 12 | 13 | // Using unknown here because of the potential 14 | // incompatibility between a locally installed TypeScript version 15 | // and the one used by the CLI. 16 | 17 | visit(program: unknown, sf: unknown): unknown; 18 | collect(): Record>; 19 | } 20 | -------------------------------------------------------------------------------- /lib/compiler/plugins/plugin-metadata-generator.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { 3 | DeepPluginMeta, 4 | ReadonlyVisitor, 5 | } from '../interfaces/readonly-visitor.interface'; 6 | import { FOUND_NO_ISSUES_GENERATING_METADATA } from '../swc/constants'; 7 | import { TypeCheckerHost } from '../swc/type-checker-host'; 8 | import { TypeScriptBinaryLoader } from '../typescript-loader'; 9 | import { PluginMetadataPrinter } from './plugin-metadata-printer'; 10 | 11 | export interface PluginMetadataGenerateOptions { 12 | /** 13 | * The visitors to use to generate the metadata. 14 | */ 15 | visitors: ReadonlyVisitor[]; 16 | /** 17 | * The output directory to write the metadata to. 18 | */ 19 | outputDir: string; 20 | /** 21 | * Whether to watch the project for changes. 22 | */ 23 | watch?: boolean; 24 | /** 25 | * The path to the tsconfig file. 26 | * Relative to the current working directory (process.cwd()). 27 | */ 28 | tsconfigPath?: string; 29 | /** 30 | * The filename to write the metadata to. 31 | */ 32 | filename?: string; 33 | /** 34 | * A reference to an existing ts.Program instance. 35 | */ 36 | tsProgramRef?: ts.Program; 37 | /** 38 | * Whether to print diagnostics to the console. 39 | * @default true 40 | */ 41 | printDiagnostics?: boolean; 42 | } 43 | 44 | /** 45 | * Generates plugins metadata by traversing the AST of the project. 46 | * @example 47 | * ```ts 48 | * const generator = new PluginMetadataGenerator(); 49 | * generator.generate({ 50 | * visitors: [ 51 | * new ReadonlyVisitor({ introspectComments: true, pathToSource: __dirname }), 52 | * ], 53 | * outputDir: __dirname, 54 | * watch: true, 55 | * tsconfigPath: 'tsconfig.build.json', 56 | * }); 57 | * ``` 58 | */ 59 | export class PluginMetadataGenerator { 60 | private readonly pluginMetadataPrinter = new PluginMetadataPrinter(); 61 | private readonly typeCheckerHost = new TypeCheckerHost(); 62 | private readonly typescriptLoader = new TypeScriptBinaryLoader(); 63 | private readonly tsBinary: typeof ts; 64 | 65 | constructor() { 66 | this.tsBinary = this.typescriptLoader.load(); 67 | } 68 | 69 | generate(options: PluginMetadataGenerateOptions) { 70 | const { 71 | tsconfigPath, 72 | visitors, 73 | tsProgramRef, 74 | outputDir, 75 | watch, 76 | filename, 77 | printDiagnostics = true, 78 | } = options; 79 | 80 | if (visitors.length === 0) { 81 | return; 82 | } 83 | 84 | if (tsProgramRef) { 85 | return this.traverseAndPrintMetadata( 86 | tsProgramRef, 87 | visitors, 88 | outputDir, 89 | filename, 90 | ); 91 | } 92 | 93 | const onTypeCheckOrProgramInit = (program: ts.Program) => { 94 | this.traverseAndPrintMetadata(program, visitors, outputDir, filename); 95 | 96 | if (printDiagnostics) { 97 | const tsBinary = this.typescriptLoader.load(); 98 | const diagnostics = tsBinary.getPreEmitDiagnostics(program); 99 | if (diagnostics.length > 0) { 100 | const formatDiagnosticsHost: ts.FormatDiagnosticsHost = { 101 | getCanonicalFileName: (path) => path, 102 | getCurrentDirectory: tsBinary.sys.getCurrentDirectory, 103 | getNewLine: () => tsBinary.sys.newLine, 104 | }; 105 | 106 | console.log(); 107 | console.log( 108 | tsBinary.formatDiagnosticsWithColorAndContext( 109 | diagnostics, 110 | formatDiagnosticsHost, 111 | ), 112 | ); 113 | } else { 114 | console.log(FOUND_NO_ISSUES_GENERATING_METADATA); 115 | } 116 | } 117 | }; 118 | this.typeCheckerHost.run(tsconfigPath, { 119 | watch, 120 | onTypeCheck: onTypeCheckOrProgramInit, 121 | onProgramInit: onTypeCheckOrProgramInit, 122 | }); 123 | } 124 | 125 | private traverseAndPrintMetadata( 126 | programRef: ts.Program, 127 | visitors: Array, 128 | outputDir: string, 129 | filename?: string, 130 | ) { 131 | for (const sourceFile of programRef.getSourceFiles()) { 132 | if (!sourceFile.isDeclarationFile) { 133 | visitors.forEach((visitor) => visitor.visit(programRef, sourceFile)); 134 | } 135 | } 136 | 137 | let typeImports: Record = {}; 138 | const collectedMetadata: Record< 139 | string, 140 | Record> 141 | > = {}; 142 | 143 | visitors.forEach((visitor) => { 144 | collectedMetadata[visitor.key] = visitor.collect() as Record< 145 | string, 146 | Array<[ts.CallExpression, DeepPluginMeta]> 147 | >; 148 | typeImports = { 149 | ...typeImports, 150 | ...visitor.typeImports, 151 | }; 152 | }); 153 | this.pluginMetadataPrinter.print( 154 | collectedMetadata, 155 | typeImports, 156 | { 157 | outputDir, 158 | filename, 159 | }, 160 | this.tsBinary, 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/compiler/plugins/plugin-metadata-printer.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { DeepPluginMeta } from '../interfaces/readonly-visitor.interface'; 5 | 6 | const SERIALIZED_METADATA_FILENAME = 'metadata.ts'; 7 | const TYPE_IMPORT_VARIABLE_NAME = 't'; 8 | 9 | export interface PluginMetadataPrintOptions { 10 | outputDir: string; 11 | filename?: string; 12 | } 13 | 14 | type ComposedPluginMeta = Record< 15 | string, 16 | Record> 17 | >; 18 | 19 | /** 20 | * Prints the metadata to a file. 21 | */ 22 | export class PluginMetadataPrinter { 23 | print( 24 | metadata: ComposedPluginMeta, 25 | typeImports: Record, 26 | options: PluginMetadataPrintOptions, 27 | tsBinary: typeof ts, 28 | ) { 29 | const objectLiteralExpr = tsBinary.factory.createObjectLiteralExpression( 30 | Object.keys(metadata).map((key) => 31 | this.recursivelyCreatePropertyAssignment( 32 | key, 33 | metadata[key] as unknown as Array< 34 | [ts.CallExpression, DeepPluginMeta] 35 | >, 36 | tsBinary, 37 | ), 38 | ), 39 | ); 40 | 41 | const exportAssignment = tsBinary.factory.createExportAssignment( 42 | undefined, 43 | undefined, 44 | tsBinary.factory.createArrowFunction( 45 | [tsBinary.factory.createToken(tsBinary.SyntaxKind.AsyncKeyword)], 46 | undefined, 47 | [], 48 | undefined, 49 | tsBinary.factory.createToken( 50 | tsBinary.SyntaxKind.EqualsGreaterThanToken, 51 | ), 52 | tsBinary.factory.createBlock( 53 | [ 54 | this.createTypeImportVariableStatement(typeImports, tsBinary), 55 | tsBinary.factory.createReturnStatement(objectLiteralExpr), 56 | ], 57 | true, 58 | ), 59 | ), 60 | ); 61 | 62 | const printer = tsBinary.createPrinter({ 63 | newLine: tsBinary.NewLineKind.LineFeed, 64 | }); 65 | const resultFile = tsBinary.createSourceFile( 66 | 'file.ts', 67 | '', 68 | tsBinary.ScriptTarget.Latest, 69 | /*setParentNodes*/ false, 70 | tsBinary.ScriptKind.TS, 71 | ); 72 | 73 | const filename = join( 74 | options.outputDir!, 75 | options.filename ?? SERIALIZED_METADATA_FILENAME, 76 | ); 77 | const eslintPrefix = `/* eslint-disable */\n`; 78 | writeFileSync( 79 | filename, 80 | eslintPrefix + 81 | printer.printNode( 82 | tsBinary.EmitHint.Unspecified, 83 | exportAssignment, 84 | resultFile, 85 | ), 86 | ); 87 | } 88 | 89 | private recursivelyCreatePropertyAssignment( 90 | identifier: string, 91 | meta: DeepPluginMeta | Array<[ts.CallExpression, DeepPluginMeta]>, 92 | tsBinary: typeof ts, 93 | ): ts.PropertyAssignment { 94 | if (Array.isArray(meta)) { 95 | return tsBinary.factory.createPropertyAssignment( 96 | tsBinary.factory.createStringLiteral(identifier), 97 | tsBinary.factory.createArrayLiteralExpression( 98 | meta.map(([importExpr, meta]) => 99 | tsBinary.factory.createArrayLiteralExpression([ 100 | importExpr, 101 | tsBinary.factory.createObjectLiteralExpression( 102 | Object.keys(meta).map((key) => 103 | this.recursivelyCreatePropertyAssignment( 104 | key, 105 | ( 106 | meta as { 107 | [key: string]: DeepPluginMeta; 108 | } 109 | )[key], 110 | tsBinary, 111 | ), 112 | ), 113 | ), 114 | ]), 115 | ), 116 | ), 117 | ); 118 | } 119 | return tsBinary.factory.createPropertyAssignment( 120 | tsBinary.factory.createStringLiteral(identifier), 121 | tsBinary.isObjectLiteralExpression(meta as unknown as ts.Node) 122 | ? (meta as ts.ObjectLiteralExpression) 123 | : tsBinary.factory.createObjectLiteralExpression( 124 | Object.keys(meta).map((key) => 125 | this.recursivelyCreatePropertyAssignment( 126 | key, 127 | ( 128 | meta as { 129 | [key: string]: DeepPluginMeta; 130 | } 131 | )[key], 132 | tsBinary, 133 | ), 134 | ), 135 | ), 136 | ); 137 | } 138 | 139 | private createTypeImportVariableStatement( 140 | typeImports: Record, 141 | tsBinary: typeof ts, 142 | ): ts.Statement { 143 | return tsBinary.factory.createVariableStatement( 144 | undefined, 145 | tsBinary.factory.createVariableDeclarationList( 146 | [ 147 | tsBinary.factory.createVariableDeclaration( 148 | tsBinary.factory.createIdentifier(TYPE_IMPORT_VARIABLE_NAME), 149 | undefined, 150 | undefined, 151 | tsBinary.factory.createObjectLiteralExpression( 152 | Object.keys(typeImports).map((ti) => 153 | this.createPropertyAssignment(ti, typeImports[ti], tsBinary), 154 | ), 155 | true, 156 | ), 157 | ), 158 | ], 159 | tsBinary.NodeFlags.Const | 160 | tsBinary.NodeFlags.AwaitContext | 161 | tsBinary.NodeFlags.ContextFlags | 162 | tsBinary.NodeFlags.TypeExcludesFlags, 163 | ), 164 | ); 165 | } 166 | 167 | private createPropertyAssignment( 168 | identifier: string, 169 | target: string, 170 | tsBinary: typeof ts, 171 | ) { 172 | return tsBinary.factory.createPropertyAssignment( 173 | tsBinary.factory.createComputedPropertyName( 174 | tsBinary.factory.createStringLiteral(identifier), 175 | ), 176 | tsBinary.factory.createIdentifier(target), 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/compiler/plugins/plugins-loader.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as ts from 'typescript'; 3 | import { CLI_ERRORS } from '../../ui'; 4 | import { ReadonlyVisitor } from '../interfaces/readonly-visitor.interface'; 5 | 6 | const PLUGIN_ENTRY_FILENAME = 'plugin'; 7 | 8 | type Transformer = ts.TransformerFactory | ts.CustomTransformerFactory; 9 | type PluginEntry = string | PluginAndOptions; 10 | type PluginOptions = Record; 11 | 12 | interface PluginAndOptions { 13 | name: 'string'; 14 | options: PluginOptions; 15 | } 16 | 17 | export interface NestCompilerPlugin { 18 | before?: (options?: PluginOptions, program?: ts.Program) => Transformer; 19 | after?: (options?: PluginOptions, program?: ts.Program) => Transformer; 20 | afterDeclarations?: ( 21 | options?: PluginOptions, 22 | program?: ts.Program, 23 | ) => Transformer; 24 | ReadonlyVisitor?: { new (options: PluginOptions): ReadonlyVisitor }; 25 | } 26 | 27 | export interface MultiNestCompilerPlugins { 28 | beforeHooks: Array<(program?: ts.Program) => Transformer>; 29 | afterHooks: Array<(program?: ts.Program) => Transformer>; 30 | afterDeclarationsHooks: Array<(program?: ts.Program) => Transformer>; 31 | readonlyVisitors: Array; 32 | } 33 | 34 | export class PluginsLoader { 35 | public load( 36 | plugins: PluginEntry[] = [], 37 | extras: { pathToSource?: string } = {}, 38 | ): MultiNestCompilerPlugins { 39 | const pluginNames = plugins.map((entry) => 40 | typeof entry === 'object' 41 | ? (entry as PluginAndOptions).name 42 | : (entry as string), 43 | ); 44 | const pluginRefs = this.resolvePluginReferences(pluginNames); 45 | const multiCompilerPlugins: MultiNestCompilerPlugins = { 46 | afterHooks: [], 47 | afterDeclarationsHooks: [], 48 | beforeHooks: [], 49 | readonlyVisitors: [], 50 | }; 51 | 52 | pluginRefs.forEach((plugin, index) => { 53 | if (!plugin.before && !plugin.after && !plugin.afterDeclarations) { 54 | throw new Error(CLI_ERRORS.WRONG_PLUGIN(pluginNames[index])); 55 | } 56 | const options = 57 | typeof plugins[index] === 'object' 58 | ? (plugins[index] as PluginAndOptions).options || {} 59 | : {}; 60 | 61 | if (plugin.before) { 62 | multiCompilerPlugins.beforeHooks.push( 63 | plugin.before.bind(plugin.before, options), 64 | ); 65 | } 66 | 67 | if (plugin.after) { 68 | multiCompilerPlugins.afterHooks.push( 69 | plugin.after.bind(plugin.after, options), 70 | ); 71 | } 72 | 73 | if (plugin.afterDeclarations) { 74 | multiCompilerPlugins.afterDeclarationsHooks.push( 75 | plugin.afterDeclarations.bind(plugin.afterDeclarations, options), 76 | ); 77 | } 78 | 79 | if (plugin.ReadonlyVisitor) { 80 | const instance = new plugin.ReadonlyVisitor({ 81 | ...options, 82 | ...extras, 83 | readonly: true, 84 | }); 85 | instance.key = pluginNames[index]; 86 | multiCompilerPlugins.readonlyVisitors.push(instance); 87 | } 88 | }); 89 | return multiCompilerPlugins; 90 | } 91 | 92 | private resolvePluginReferences(pluginNames: string[]): NestCompilerPlugin[] { 93 | const nodeModulePaths = [ 94 | join(process.cwd(), 'node_modules'), 95 | ...module.paths, 96 | ]; 97 | 98 | return pluginNames.map((item) => { 99 | try { 100 | try { 101 | const binaryPath = require.resolve( 102 | join(item, PLUGIN_ENTRY_FILENAME), 103 | { 104 | paths: nodeModulePaths, 105 | }, 106 | ); 107 | return require(binaryPath); 108 | } catch {} 109 | 110 | const binaryPath = require.resolve(item, { paths: nodeModulePaths }); 111 | return require(binaryPath); 112 | } catch (e) { 113 | throw new Error(`"${item}" plugin is not installed.`); 114 | } 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/compiler/swc/constants.ts: -------------------------------------------------------------------------------- 1 | import { bgCyan, bgGreen, bgRed, cyan, green, red } from 'ansis'; 2 | 3 | export const TSC_NO_ERRORS_MESSAGE = 4 | 'Found 0 errors. Watching for file changes.'; 5 | 6 | export const TSC_COMPILATION_STARTED_MESSAGE = 7 | 'Starting compilation in watch mode...'; 8 | 9 | export const SWC_LOG_PREFIX = cyan('> ') + bgCyan.bold(' SWC '); 10 | 11 | export const TSC_LOG_PREFIX = cyan('> ') + bgCyan.bold(' TSC '); 12 | export const TSC_LOG_ERROR_PREFIX = red('> ') + bgRed.bold(' TSC '); 13 | export const TSC_LOG_SUCCESS_PREFIX = green('> ') + bgGreen.bold(' TSC '); 14 | 15 | export const INITIALIZING_TYPE_CHECKER = 16 | bgCyan.bold(' TSC ') + cyan(' Initializing type checker...'); 17 | 18 | export const FOUND_NO_ISSUES_METADATA_GENERATION_SKIPPED = 19 | TSC_LOG_SUCCESS_PREFIX + green(' Found 0 issues.'); 20 | 21 | export const FOUND_NO_ISSUES_GENERATING_METADATA = 22 | TSC_LOG_SUCCESS_PREFIX + green(' Found 0 issues. Generating metadata...'); 23 | -------------------------------------------------------------------------------- /lib/compiler/swc/forked-type-checker.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Configuration } from '../../configuration'; 3 | import { ERROR_PREFIX } from '../../ui'; 4 | import { BaseCompiler } from '../base-compiler'; 5 | import { PluginMetadataGenerator } from '../plugins/plugin-metadata-generator'; 6 | import { PluginsLoader } from '../plugins/plugins-loader'; 7 | import { 8 | FOUND_NO_ISSUES_GENERATING_METADATA, 9 | FOUND_NO_ISSUES_METADATA_GENERATION_SKIPPED, 10 | } from './constants'; 11 | import { SwcCompilerExtras } from './swc-compiler'; 12 | import { TypeCheckerHost } from './type-checker-host'; 13 | 14 | const [tsConfigPath, appName, sourceRoot, plugins] = process.argv.slice(2); 15 | 16 | class ForkedTypeChecker extends BaseCompiler { 17 | private readonly pluginMetadataGenerator = new PluginMetadataGenerator(); 18 | private readonly typeCheckerHost = new TypeCheckerHost(); 19 | 20 | public async run( 21 | configuration: Required, 22 | tsConfigPath: string, 23 | appName: string | undefined, 24 | extras: Pick, 25 | ) { 26 | const { readonlyVisitors } = this.loadPlugins( 27 | configuration, 28 | tsConfigPath, 29 | appName, 30 | ); 31 | const outputDir = this.getPathToSource( 32 | configuration, 33 | tsConfigPath, 34 | appName, 35 | ); 36 | 37 | try { 38 | const onTypeCheckOrProgramInit = (program: ts.Program) => { 39 | if (readonlyVisitors.length > 0) { 40 | console.log(FOUND_NO_ISSUES_GENERATING_METADATA); 41 | 42 | this.pluginMetadataGenerator.generate({ 43 | outputDir, 44 | visitors: readonlyVisitors, 45 | tsProgramRef: program, 46 | }); 47 | } else { 48 | console.log(FOUND_NO_ISSUES_METADATA_GENERATION_SKIPPED); 49 | } 50 | }; 51 | this.typeCheckerHost.run(tsConfigPath, { 52 | watch: extras.watch, 53 | onTypeCheck: onTypeCheckOrProgramInit, 54 | onProgramInit: onTypeCheckOrProgramInit, 55 | }); 56 | } catch (err) { 57 | console.log(ERROR_PREFIX, err.message); 58 | } 59 | } 60 | } 61 | 62 | const pluginsLoader = new PluginsLoader(); 63 | const forkedTypeChecker = new ForkedTypeChecker(pluginsLoader); 64 | const applicationName = appName === 'undefined' ? '' : appName; 65 | const options: Partial = { 66 | sourceRoot, 67 | }; 68 | 69 | if (applicationName) { 70 | options.projects = {}; 71 | options.projects[applicationName] = { 72 | compilerOptions: { 73 | plugins: JSON.parse(plugins), 74 | }, 75 | }; 76 | } else { 77 | options.compilerOptions = { 78 | plugins: JSON.parse(plugins), 79 | }; 80 | } 81 | 82 | forkedTypeChecker.run( 83 | options as unknown as Required, 84 | tsConfigPath, 85 | applicationName, 86 | { watch: true, typeCheck: true }, 87 | ); 88 | -------------------------------------------------------------------------------- /lib/compiler/swc/type-checker-host.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'ansis'; 2 | import * as ora from 'ora'; 3 | import * as ts from 'typescript'; 4 | import { TsConfigProvider } from '../helpers/tsconfig-provider'; 5 | import { TypeScriptBinaryLoader } from '../typescript-loader'; 6 | import { 7 | INITIALIZING_TYPE_CHECKER, 8 | TSC_LOG_ERROR_PREFIX, 9 | TSC_NO_ERRORS_MESSAGE, 10 | } from './constants'; 11 | 12 | export interface TypeCheckerHostRunOptions { 13 | watch?: boolean; 14 | onTypeCheck?: (program: ts.Program) => void; 15 | onProgramInit?: (program: ts.Program) => void; 16 | } 17 | 18 | export class TypeCheckerHost { 19 | private readonly typescriptLoader = new TypeScriptBinaryLoader(); 20 | private readonly tsConfigProvider = new TsConfigProvider( 21 | this.typescriptLoader, 22 | ); 23 | 24 | public run( 25 | tsconfigPath: string | undefined, 26 | options: TypeCheckerHostRunOptions, 27 | ) { 28 | if (!tsconfigPath) { 29 | throw new Error( 30 | '"tsconfigPath" is required when "tsProgramRef" is not provided.', 31 | ); 32 | } 33 | const tsBinary = this.typescriptLoader.load(); 34 | 35 | const spinner = ora({ 36 | text: INITIALIZING_TYPE_CHECKER, 37 | }); 38 | 39 | if (options.watch) { 40 | console.log(); 41 | 42 | spinner.start(); 43 | this.runInWatchMode(tsconfigPath, tsBinary, options); 44 | spinner.succeed(); 45 | return; 46 | } 47 | spinner.start(); 48 | this.runOnce(tsconfigPath, tsBinary, options); 49 | spinner.succeed(); 50 | } 51 | 52 | private runInWatchMode( 53 | tsconfigPath: string, 54 | tsBinary: typeof ts, 55 | options: TypeCheckerHostRunOptions, 56 | ) { 57 | const { options: tsOptions } = 58 | this.tsConfigProvider.getByConfigFilename(tsconfigPath); 59 | 60 | let builderProgram: ts.WatchOfConfigFile | undefined = 61 | undefined; 62 | 63 | const reportWatchStatusCallback = (diagnostic: ts.Diagnostic) => { 64 | if (diagnostic.messageText !== TSC_NO_ERRORS_MESSAGE) { 65 | if ((diagnostic.messageText as string)?.includes('Found')) { 66 | console.log(TSC_LOG_ERROR_PREFIX, red(diagnostic.messageText.toString())); 67 | } 68 | return; 69 | } 70 | if (!builderProgram) { 71 | return; 72 | } 73 | const tsProgram = builderProgram.getProgram().getProgram(); 74 | options.onTypeCheck?.(tsProgram); 75 | }; 76 | 77 | const host = this.createWatchCompilerHost( 78 | tsBinary, 79 | tsconfigPath, 80 | tsOptions, 81 | reportWatchStatusCallback, 82 | ); 83 | builderProgram = tsBinary.createWatchProgram(host); 84 | process.nextTick(() => { 85 | options.onProgramInit?.(builderProgram!.getProgram().getProgram()); 86 | }); 87 | } 88 | 89 | private runOnce( 90 | tsconfigPath: string, 91 | tsBinary: typeof ts, 92 | options: TypeCheckerHostRunOptions, 93 | ) { 94 | const { 95 | options: tsOptions, 96 | fileNames, 97 | projectReferences, 98 | } = this.tsConfigProvider.getByConfigFilename(tsconfigPath); 99 | 100 | const createProgram = 101 | tsBinary.createIncrementalProgram ?? tsBinary.createProgram; 102 | 103 | const program = createProgram.call(ts, { 104 | rootNames: fileNames, 105 | projectReferences, 106 | options: tsOptions, 107 | }); 108 | 109 | const programRef = program.getProgram 110 | ? program.getProgram() 111 | : (program as any as ts.Program); 112 | 113 | const diagnostics = tsBinary.getPreEmitDiagnostics(programRef); 114 | if (diagnostics.length > 0) { 115 | const formatDiagnosticsHost: ts.FormatDiagnosticsHost = { 116 | getCanonicalFileName: (path) => path, 117 | getCurrentDirectory: tsBinary.sys.getCurrentDirectory, 118 | getNewLine: () => tsBinary.sys.newLine, 119 | }; 120 | 121 | console.log(); 122 | console.log( 123 | tsBinary.formatDiagnosticsWithColorAndContext( 124 | diagnostics, 125 | formatDiagnosticsHost, 126 | ), 127 | ); 128 | process.exit(1); 129 | } 130 | options.onTypeCheck?.(programRef); 131 | } 132 | 133 | private createWatchCompilerHost( 134 | tsBinary: typeof ts, 135 | tsConfigPath: string, 136 | options: ts.CompilerOptions, 137 | reportWatchStatusCallback: ts.WatchStatusReporter, 138 | ) { 139 | const origDiagnosticReporter = (tsBinary as any).createDiagnosticReporter( 140 | tsBinary.sys, 141 | true, 142 | ); 143 | 144 | const tsOptions = { 145 | ...options, 146 | preserveWatchOutput: true, 147 | noEmit: true, 148 | }; 149 | 150 | return tsBinary.createWatchCompilerHost( 151 | tsConfigPath, 152 | tsOptions, 153 | tsBinary.sys, 154 | undefined, 155 | origDiagnosticReporter, 156 | reportWatchStatusCallback, 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/compiler/typescript-loader.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export class TypeScriptBinaryLoader { 4 | private tsBinary?: typeof ts; 5 | 6 | public load(): typeof ts { 7 | if (this.tsBinary) { 8 | return this.tsBinary; 9 | } 10 | 11 | try { 12 | const tsBinaryPath = require.resolve('typescript', { 13 | paths: [process.cwd(), ...this.getModulePaths()], 14 | }); 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires 16 | const tsBinary = require(tsBinaryPath); 17 | this.tsBinary = tsBinary; 18 | return tsBinary; 19 | } catch { 20 | throw new Error( 21 | 'TypeScript could not be found! Please, install "typescript" package.', 22 | ); 23 | } 24 | } 25 | 26 | public getModulePaths() { 27 | const modulePaths = module.paths.slice(2, module.paths.length); 28 | const packageDeps = modulePaths.slice(0, 3); 29 | return [ 30 | ...packageDeps.reverse(), 31 | ...modulePaths.slice(3, modulePaths.length).reverse(), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/compiler/webpack-compiler.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { Input } from '../../commands'; 4 | import { Configuration } from '../configuration'; 5 | import { INFO_PREFIX } from '../ui'; 6 | import { AssetsManager } from './assets-manager'; 7 | import { BaseCompiler } from './base-compiler'; 8 | import { webpackDefaultsFactory } from './defaults/webpack-defaults'; 9 | import { getValueOrDefault } from './helpers/get-value-or-default'; 10 | import { PluginsLoader } from './plugins/plugins-loader'; 11 | import webpack = require('webpack'); 12 | 13 | type WebpackConfigFactory = ( 14 | config: webpack.Configuration, 15 | webpackRef: typeof webpack, 16 | ) => webpack.Configuration; 17 | 18 | type WebpackConfigFactoryOrConfig = 19 | | WebpackConfigFactory 20 | | webpack.Configuration; 21 | 22 | type WebpackCompilerExtras = { 23 | inputs: Input[]; 24 | assetsManager: AssetsManager; 25 | webpackConfigFactoryOrConfig: 26 | | WebpackConfigFactoryOrConfig 27 | | WebpackConfigFactoryOrConfig[]; 28 | debug?: boolean; 29 | watchMode?: boolean; 30 | }; 31 | 32 | export class WebpackCompiler extends BaseCompiler { 33 | constructor(pluginsLoader: PluginsLoader) { 34 | super(pluginsLoader); 35 | } 36 | 37 | public run( 38 | configuration: Required, 39 | tsConfigPath: string, 40 | appName: string | undefined, 41 | extras: WebpackCompilerExtras, 42 | onSuccess?: () => void, 43 | ) { 44 | const cwd = process.cwd(); 45 | const configPath = join(cwd, tsConfigPath!); 46 | if (!existsSync(configPath)) { 47 | throw new Error( 48 | `Could not find TypeScript configuration file "${tsConfigPath!}".`, 49 | ); 50 | } 51 | 52 | const plugins = this.loadPlugins(configuration, tsConfigPath, appName); 53 | const pathToSource = this.getPathToSource( 54 | configuration, 55 | tsConfigPath, 56 | appName, 57 | ); 58 | 59 | const entryFile = getValueOrDefault( 60 | configuration, 61 | 'entryFile', 62 | appName, 63 | 'entryFile', 64 | extras.inputs, 65 | ); 66 | const entryFileRoot = 67 | getValueOrDefault(configuration, 'root', appName) || ''; 68 | const defaultOptions = webpackDefaultsFactory( 69 | pathToSource, 70 | entryFileRoot, 71 | entryFile, 72 | extras.debug ?? false, 73 | tsConfigPath, 74 | plugins, 75 | ); 76 | 77 | let compiler: webpack.Compiler | webpack.MultiCompiler; 78 | let watchOptions: 79 | | Parameters[0] 80 | | undefined; 81 | let watch: boolean | undefined; 82 | 83 | if (Array.isArray(extras.webpackConfigFactoryOrConfig)) { 84 | const webpackConfigurations = extras.webpackConfigFactoryOrConfig.map( 85 | (configOrFactory) => { 86 | const unwrappedConfig = 87 | typeof configOrFactory !== 'function' 88 | ? configOrFactory 89 | : configOrFactory(defaultOptions, webpack); 90 | return { 91 | ...defaultOptions, 92 | mode: extras.watchMode ? 'development' : defaultOptions.mode, 93 | ...unwrappedConfig, 94 | }; 95 | }, 96 | ); 97 | compiler = webpack(webpackConfigurations); 98 | watchOptions = webpackConfigurations.map( 99 | (config) => config.watchOptions || {}, 100 | ); 101 | watch = webpackConfigurations.some((config) => config.watch); 102 | } else { 103 | const projectWebpackOptions = 104 | typeof extras.webpackConfigFactoryOrConfig !== 'function' 105 | ? extras.webpackConfigFactoryOrConfig 106 | : extras.webpackConfigFactoryOrConfig(defaultOptions, webpack); 107 | const webpackConfiguration = { 108 | ...defaultOptions, 109 | mode: extras.watchMode ? 'development' : defaultOptions.mode, 110 | ...projectWebpackOptions, 111 | }; 112 | compiler = webpack(webpackConfiguration); 113 | watchOptions = webpackConfiguration.watchOptions; 114 | watch = webpackConfiguration.watch; 115 | } 116 | 117 | const afterCallback = this.createAfterCallback( 118 | onSuccess, 119 | extras.assetsManager, 120 | extras.watchMode ?? false, 121 | watch, 122 | ); 123 | 124 | if (extras.watchMode || watch) { 125 | compiler.hooks.watchRun.tapAsync('Rebuild info', (params, callback) => { 126 | console.log(`\n${INFO_PREFIX} Webpack is building your sources...\n`); 127 | callback(); 128 | }); 129 | compiler.watch(watchOptions! || {}, afterCallback); 130 | } else { 131 | compiler.run(afterCallback); 132 | } 133 | } 134 | 135 | private createAfterCallback( 136 | onSuccess: (() => void) | undefined, 137 | assetsManager: AssetsManager, 138 | watchMode: boolean, 139 | watch: boolean | undefined, 140 | ) { 141 | return ( 142 | err: Error | null | undefined, 143 | stats: webpack.Stats | webpack.MultiStats | undefined, 144 | ) => { 145 | if (err && stats === undefined) { 146 | // Could not complete the compilation 147 | // The error caught is most likely thrown by underlying tasks 148 | console.log(err); 149 | return process.exit(1); 150 | } 151 | const statsOutput = stats!.toString({ 152 | chunks: false, 153 | colors: true, 154 | modules: false, 155 | assets: false, 156 | }); 157 | if (!err && !stats!.hasErrors()) { 158 | if (!onSuccess) { 159 | assetsManager.closeWatchers(); 160 | } else { 161 | onSuccess(); 162 | } 163 | } else if (!watchMode && !watch) { 164 | console.log(statsOutput); 165 | return process.exit(1); 166 | } 167 | console.log(statsOutput); 168 | }; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/configuration/configuration.loader.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from './configuration'; 2 | 3 | export interface ConfigurationLoader { 4 | load( 5 | name?: string, 6 | ): Required | Promise>; 7 | } 8 | -------------------------------------------------------------------------------- /lib/configuration/configuration.ts: -------------------------------------------------------------------------------- 1 | export type Asset = 'string' | AssetEntry; 2 | export interface AssetEntry { 3 | glob: string; 4 | include?: string; 5 | flat?: boolean; 6 | exclude?: string; 7 | outDir?: string; 8 | watchAssets?: boolean; 9 | } 10 | 11 | export interface ActionOnFile { 12 | action: 'change' | 'unlink'; 13 | item: AssetEntry; 14 | path: string; 15 | sourceRoot: string; 16 | watchAssetsMode: boolean; 17 | } 18 | 19 | export interface SwcBuilderOptions { 20 | swcrcPath?: string; 21 | outDir?: string; 22 | filenames?: string[]; 23 | sync?: boolean; 24 | extensions?: string[]; 25 | copyFiles?: boolean; 26 | includeDotfiles?: boolean; 27 | quiet?: boolean; 28 | } 29 | 30 | export interface WebpackBuilderOptions { 31 | configPath?: string; 32 | } 33 | 34 | export interface TscBuilderOptions { 35 | configPath?: string; 36 | } 37 | 38 | export type BuilderVariant = 'tsc' | 'swc' | 'webpack'; 39 | export type Builder = 40 | | BuilderVariant 41 | | { 42 | type: 'webpack'; 43 | options?: WebpackBuilderOptions; 44 | } 45 | | { 46 | type: 'swc'; 47 | options?: SwcBuilderOptions; 48 | } 49 | | { 50 | type: 'tsc'; 51 | options?: TscBuilderOptions; 52 | }; 53 | 54 | export interface CompilerOptions { 55 | tsConfigPath?: string; 56 | /** 57 | * @deprecated Use `builder` instead. 58 | */ 59 | webpack?: boolean; 60 | /** 61 | * @deprecated Use `builder.options.configPath` instead. 62 | */ 63 | webpackConfigPath?: string; 64 | plugins?: string[] | PluginOptions[]; 65 | assets?: string[]; 66 | deleteOutDir?: boolean; 67 | manualRestart?: boolean; 68 | builder?: Builder; 69 | } 70 | 71 | export interface PluginOptions { 72 | name: string; 73 | options: Record[]; 74 | } 75 | 76 | export interface GenerateOptions { 77 | spec?: boolean | Record; 78 | flat?: boolean; 79 | specFileSuffix?: string; 80 | baseDir?: string; 81 | } 82 | 83 | export interface ProjectConfiguration { 84 | type?: string; 85 | root?: string; 86 | entryFile?: string; 87 | exec?: string; 88 | sourceRoot?: string; 89 | compilerOptions?: CompilerOptions; 90 | } 91 | 92 | export interface Configuration { 93 | [key: string]: any; 94 | language?: string; 95 | collection?: string; 96 | sourceRoot?: string; 97 | entryFile?: string; 98 | exec?: string; 99 | monorepo?: boolean; 100 | compilerOptions?: CompilerOptions; 101 | generateOptions?: GenerateOptions; 102 | projects?: { 103 | [key: string]: ProjectConfiguration; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /lib/configuration/defaults.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultTsconfigPath } from '../utils/get-default-tsconfig-path'; 2 | import { Configuration } from './configuration'; 3 | 4 | export const defaultConfiguration: Required = { 5 | language: 'ts', 6 | sourceRoot: 'src', 7 | collection: '@nestjs/schematics', 8 | entryFile: 'main', 9 | exec: 'node', 10 | projects: {}, 11 | monorepo: false, 12 | compilerOptions: { 13 | builder: { 14 | type: 'tsc', 15 | options: { 16 | configPath: getDefaultTsconfigPath(), 17 | }, 18 | }, 19 | webpack: false, 20 | plugins: [], 21 | assets: [], 22 | manualRestart: false, 23 | }, 24 | generateOptions: {}, 25 | }; 26 | 27 | export const defaultTsconfigFilename = getDefaultTsconfigPath(); 28 | export const defaultWebpackConfigFilename = 'webpack.config.js'; 29 | export const defaultOutDir = 'dist'; 30 | export const defaultGitIgnore = `# compiled output 31 | /dist 32 | /node_modules 33 | /build 34 | 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | pnpm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | lerna-debug.log* 43 | 44 | # OS 45 | .DS_Store 46 | 47 | # Tests 48 | /coverage 49 | /.nyc_output 50 | 51 | # IDEs and editors 52 | /.idea 53 | .project 54 | .classpath 55 | .c9/ 56 | *.launch 57 | .settings/ 58 | *.sublime-workspace 59 | 60 | # IDE - VSCode 61 | .vscode/* 62 | !.vscode/settings.json 63 | !.vscode/tasks.json 64 | !.vscode/launch.json 65 | !.vscode/extensions.json 66 | 67 | # dotenv environment variable files 68 | .env 69 | .env.development.local 70 | .env.test.local 71 | .env.production.local 72 | .env.local 73 | 74 | # temp directory 75 | .temp 76 | .tmp 77 | 78 | # Runtime data 79 | pids 80 | *.pid 81 | *.seed 82 | *.pid.lock 83 | 84 | # Diagnostic reports (https://nodejs.org/api/report.html) 85 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 86 | `; 87 | -------------------------------------------------------------------------------- /lib/configuration/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configuration.loader'; 2 | export * from './nest-configuration.loader'; 3 | export * from './configuration'; 4 | -------------------------------------------------------------------------------- /lib/configuration/nest-configuration.loader.ts: -------------------------------------------------------------------------------- 1 | import { Reader, ReaderFileLackPermissionsError } from '../readers'; 2 | import { Configuration } from './configuration'; 3 | import { ConfigurationLoader } from './configuration.loader'; 4 | import { defaultConfiguration } from './defaults'; 5 | 6 | /** 7 | * A cache table that maps some reader (by its name along with the config path) 8 | * to a loaded configuration. 9 | * This was added because several commands relies on the app's config in order 10 | * to generate some dynanmic content prior running the command itself. 11 | */ 12 | const loadedConfigsCache = new Map>(); 13 | 14 | export class NestConfigurationLoader implements ConfigurationLoader { 15 | constructor(private readonly reader: Reader) {} 16 | 17 | public load(name?: string): Required { 18 | const cacheEntryKey = `${this.reader.constructor.name}:${name}`; 19 | const cachedConfig = loadedConfigsCache.get(cacheEntryKey); 20 | if (cachedConfig) { 21 | return cachedConfig; 22 | } 23 | 24 | let loadedConfig: Required | undefined; 25 | 26 | const contentOrError = name 27 | ? this.reader.read(name) 28 | : this.reader.readAnyOf([ 29 | 'nest-cli.json', 30 | '.nest-cli.json', 31 | ]); 32 | 33 | if (contentOrError) { 34 | const isMissingPermissionsError = 35 | contentOrError instanceof ReaderFileLackPermissionsError; 36 | if (isMissingPermissionsError) { 37 | console.error(contentOrError.message); 38 | process.exit(1); 39 | } 40 | 41 | const fileConfig = JSON.parse(contentOrError); 42 | if (fileConfig.compilerOptions) { 43 | loadedConfig = { 44 | ...defaultConfiguration, 45 | ...fileConfig, 46 | compilerOptions: { 47 | ...defaultConfiguration.compilerOptions, 48 | ...fileConfig.compilerOptions, 49 | }, 50 | }; 51 | } else { 52 | loadedConfig = { 53 | ...defaultConfiguration, 54 | ...fileConfig, 55 | }; 56 | } 57 | } else { 58 | loadedConfig = defaultConfiguration; 59 | } 60 | 61 | loadedConfigsCache.set(cacheEntryKey, loadedConfig!); 62 | return loadedConfig!; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/package-managers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './package-manager'; 2 | export * from './package-manager.factory'; 3 | export * from './abstract.package-manager'; 4 | export * from './npm.package-manager'; 5 | export * from './yarn.package-manager'; 6 | export * from './pnpm.package-manager'; 7 | export * from './project.dependency'; 8 | export * from './package-manager-commands'; 9 | -------------------------------------------------------------------------------- /lib/package-managers/npm.package-manager.ts: -------------------------------------------------------------------------------- 1 | import { Runner, RunnerFactory } from '../runners'; 2 | import { NpmRunner } from '../runners/npm.runner'; 3 | import { AbstractPackageManager } from './abstract.package-manager'; 4 | import { PackageManager } from './package-manager'; 5 | import { PackageManagerCommands } from './package-manager-commands'; 6 | 7 | export class NpmPackageManager extends AbstractPackageManager { 8 | constructor() { 9 | super(RunnerFactory.create(Runner.NPM) as NpmRunner); 10 | } 11 | 12 | public get name() { 13 | return PackageManager.NPM.toUpperCase(); 14 | } 15 | 16 | get cli(): PackageManagerCommands { 17 | return { 18 | install: 'install', 19 | add: 'install', 20 | update: 'update', 21 | remove: 'uninstall', 22 | saveFlag: '--save', 23 | saveDevFlag: '--save-dev', 24 | silentFlag: '--silent', 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/package-managers/package-manager-commands.ts: -------------------------------------------------------------------------------- 1 | export interface PackageManagerCommands { 2 | install: string; 3 | add: string; 4 | update: string; 5 | remove: string; 6 | saveFlag: string; 7 | saveDevFlag: string; 8 | silentFlag: string; 9 | } 10 | -------------------------------------------------------------------------------- /lib/package-managers/package-manager.factory.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { AbstractPackageManager } from './abstract.package-manager'; 3 | import { NpmPackageManager } from './npm.package-manager'; 4 | import { PackageManager } from './package-manager'; 5 | import { YarnPackageManager } from './yarn.package-manager'; 6 | import { PnpmPackageManager } from './pnpm.package-manager'; 7 | 8 | export class PackageManagerFactory { 9 | public static create(name: PackageManager | string): AbstractPackageManager { 10 | switch (name) { 11 | case PackageManager.NPM: 12 | return new NpmPackageManager(); 13 | case PackageManager.YARN: 14 | return new YarnPackageManager(); 15 | case PackageManager.PNPM: 16 | return new PnpmPackageManager(); 17 | default: 18 | throw new Error(`Package manager ${name} is not managed.`); 19 | } 20 | } 21 | 22 | public static async find(): Promise { 23 | const DEFAULT_PACKAGE_MANAGER = PackageManager.NPM; 24 | 25 | try { 26 | const files = await fs.promises.readdir(process.cwd()); 27 | 28 | const hasYarnLockFile = files.includes('yarn.lock'); 29 | if (hasYarnLockFile) { 30 | return this.create(PackageManager.YARN); 31 | } 32 | 33 | const hasPnpmLockFile = files.includes('pnpm-lock.yaml'); 34 | if (hasPnpmLockFile) { 35 | return this.create(PackageManager.PNPM); 36 | } 37 | 38 | return this.create(DEFAULT_PACKAGE_MANAGER); 39 | } catch (error) { 40 | return this.create(DEFAULT_PACKAGE_MANAGER); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/package-managers/package-manager.ts: -------------------------------------------------------------------------------- 1 | export enum PackageManager { 2 | NPM = 'npm', 3 | YARN = 'yarn', 4 | PNPM = 'pnpm', 5 | } 6 | -------------------------------------------------------------------------------- /lib/package-managers/pnpm.package-manager.ts: -------------------------------------------------------------------------------- 1 | import { Runner, RunnerFactory } from '../runners'; 2 | import { PnpmRunner } from '../runners/pnpm.runner'; 3 | import { AbstractPackageManager } from './abstract.package-manager'; 4 | import { PackageManager } from './package-manager'; 5 | import { PackageManagerCommands } from './package-manager-commands'; 6 | 7 | export class PnpmPackageManager extends AbstractPackageManager { 8 | constructor() { 9 | super(RunnerFactory.create(Runner.PNPM) as PnpmRunner); 10 | } 11 | 12 | public get name() { 13 | return PackageManager.PNPM.toUpperCase(); 14 | } 15 | 16 | // As of PNPM v5.3, all commands are shared with NPM v6.14.5. See: https://pnpm.js.org/en/pnpm-vs-npm 17 | get cli(): PackageManagerCommands { 18 | return { 19 | install: 'install --strict-peer-dependencies=false', 20 | add: 'install --strict-peer-dependencies=false', 21 | update: 'update', 22 | remove: 'uninstall', 23 | saveFlag: '--save', 24 | saveDevFlag: '--save-dev', 25 | silentFlag: '--reporter=silent', 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/package-managers/project.dependency.ts: -------------------------------------------------------------------------------- 1 | export interface ProjectDependency { 2 | name: string; 3 | version: string; 4 | } 5 | -------------------------------------------------------------------------------- /lib/package-managers/yarn.package-manager.ts: -------------------------------------------------------------------------------- 1 | import { Runner, RunnerFactory } from '../runners'; 2 | import { YarnRunner } from '../runners/yarn.runner'; 3 | import { AbstractPackageManager } from './abstract.package-manager'; 4 | import { PackageManager } from './package-manager'; 5 | import { PackageManagerCommands } from './package-manager-commands'; 6 | 7 | export class YarnPackageManager extends AbstractPackageManager { 8 | constructor() { 9 | super(RunnerFactory.create(Runner.YARN) as YarnRunner); 10 | } 11 | 12 | public get name() { 13 | return PackageManager.YARN.toUpperCase(); 14 | } 15 | 16 | get cli(): PackageManagerCommands { 17 | return { 18 | install: 'install', 19 | add: 'add', 20 | update: 'upgrade', 21 | remove: 'remove', 22 | saveFlag: '', 23 | saveDevFlag: '-D', 24 | silentFlag: '--silent', 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/questions/questions.ts: -------------------------------------------------------------------------------- 1 | export const generateInput = (name: string, message: string) => { 2 | return (defaultAnswer: string): any => ({ 3 | name, 4 | message, 5 | default: defaultAnswer, 6 | }); 7 | }; 8 | 9 | export const generateSelect = (name: string) => { 10 | return (message: string) => { 11 | return (choices: string[]) => { 12 | const choicesFormatted = choices.map((choice) => ({ 13 | name: choice, 14 | value: choice, 15 | })); 16 | return { 17 | name, 18 | message, 19 | choices: choicesFormatted, 20 | }; 21 | }; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/readers/file-system.reader.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Reader, ReaderFileLackPermissionsError } from './reader'; 4 | 5 | export class FileSystemReader implements Reader { 6 | constructor(private readonly directory: string) {} 7 | 8 | public list(): string[] { 9 | return fs.readdirSync(this.directory); 10 | } 11 | 12 | public read(name: string): string { 13 | return fs.readFileSync(path.join(this.directory, name), 'utf8'); 14 | } 15 | 16 | public readAnyOf( 17 | filenames: string[], 18 | ): string | undefined | ReaderFileLackPermissionsError { 19 | let firstFilePathFoundButWithInsufficientPermissions: string | undefined; 20 | 21 | for (let id = 0; id < filenames.length; id++) { 22 | const file = filenames[id]; 23 | 24 | try { 25 | return this.read(file); 26 | } catch (readErr) { 27 | if ( 28 | !firstFilePathFoundButWithInsufficientPermissions && 29 | typeof readErr?.code === 'string' 30 | ) { 31 | const isInsufficientPermissionsError = 32 | readErr.code === 'EACCES' || readErr.code === 'EPERM'; 33 | if (isInsufficientPermissionsError) { 34 | firstFilePathFoundButWithInsufficientPermissions = readErr.path; 35 | } 36 | } 37 | 38 | const isLastFileToLookFor = id === filenames.length - 1; 39 | if (!isLastFileToLookFor) { 40 | continue; 41 | } 42 | 43 | if (firstFilePathFoundButWithInsufficientPermissions) { 44 | return new ReaderFileLackPermissionsError( 45 | firstFilePathFoundButWithInsufficientPermissions, 46 | readErr.code, 47 | ); 48 | } else { 49 | return undefined; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/readers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reader'; 2 | export * from './file-system.reader'; 3 | -------------------------------------------------------------------------------- /lib/readers/reader.ts: -------------------------------------------------------------------------------- 1 | export class ReaderFileLackPermissionsError extends Error { 2 | constructor( 3 | public readonly filePath: string, 4 | public readonly fsErrorCode: string, 5 | ) { 6 | super(`File ${filePath} lacks read permissions!`); 7 | } 8 | } 9 | 10 | export interface Reader { 11 | list(): string[]; 12 | read(name: string): string; 13 | readAnyOf( 14 | filenames: string[], 15 | ): string | undefined | ReaderFileLackPermissionsError; 16 | } 17 | -------------------------------------------------------------------------------- /lib/runners/abstract.runner.ts: -------------------------------------------------------------------------------- 1 | import { red } from 'ansis'; 2 | import { ChildProcess, spawn, SpawnOptions } from 'child_process'; 3 | import { MESSAGES } from '../ui'; 4 | 5 | export class AbstractRunner { 6 | constructor( 7 | protected binary: string, 8 | protected args: string[] = [], 9 | ) {} 10 | 11 | public async run( 12 | command: string, 13 | collect = false, 14 | cwd: string = process.cwd(), 15 | ): Promise { 16 | const args: string[] = [command]; 17 | const options: SpawnOptions = { 18 | cwd, 19 | stdio: collect ? 'pipe' : 'inherit', 20 | shell: true, 21 | }; 22 | return new Promise((resolve, reject) => { 23 | const child: ChildProcess = spawn( 24 | `${this.binary}`, 25 | [...this.args, ...args], 26 | options, 27 | ); 28 | if (collect) { 29 | child.stdout!.on('data', (data) => 30 | resolve(data.toString().replace(/\r\n|\n/, '')), 31 | ); 32 | } 33 | child.on('close', (code) => { 34 | if (code === 0) { 35 | resolve(null); 36 | } else { 37 | console.error( 38 | red( 39 | MESSAGES.RUNNER_EXECUTION_ERROR(`${this.binary} ${command}`), 40 | ), 41 | ); 42 | reject(); 43 | } 44 | }); 45 | }); 46 | } 47 | 48 | /** 49 | * @param command 50 | * @returns The entire command that will be ran when calling `run(command)`. 51 | */ 52 | public rawFullCommand(command: string): string { 53 | const commandArgs: string[] = [...this.args, command]; 54 | return `${this.binary} ${commandArgs.join(' ')}`; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/runners/git.runner.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from './abstract.runner'; 2 | 3 | export class GitRunner extends AbstractRunner { 4 | constructor() { 5 | super('git'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/runners/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runner'; 2 | export * from './runner.factory'; 3 | export * from './abstract.runner'; 4 | -------------------------------------------------------------------------------- /lib/runners/npm.runner.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from './abstract.runner'; 2 | 3 | export class NpmRunner extends AbstractRunner { 4 | constructor() { 5 | super('npm'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/runners/pnpm.runner.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from './abstract.runner'; 2 | 3 | export class PnpmRunner extends AbstractRunner { 4 | constructor() { 5 | super('pnpm'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/runners/runner.factory.ts: -------------------------------------------------------------------------------- 1 | import { yellow } from 'ansis'; 2 | import { NpmRunner } from './npm.runner'; 3 | import { Runner } from './runner'; 4 | import { SchematicRunner } from './schematic.runner'; 5 | import { YarnRunner } from './yarn.runner'; 6 | import { PnpmRunner } from './pnpm.runner'; 7 | 8 | export class RunnerFactory { 9 | public static create(runner: Runner) { 10 | switch (runner) { 11 | case Runner.SCHEMATIC: 12 | return new SchematicRunner(); 13 | 14 | case Runner.NPM: 15 | return new NpmRunner(); 16 | 17 | case Runner.YARN: 18 | return new YarnRunner(); 19 | 20 | case Runner.PNPM: 21 | return new PnpmRunner(); 22 | 23 | default: 24 | console.info(yellow`[WARN] Unsupported runner: ${runner}`); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/runners/runner.ts: -------------------------------------------------------------------------------- 1 | export enum Runner { 2 | SCHEMATIC, 3 | NPM, 4 | YARN, 5 | PNPM, 6 | } 7 | -------------------------------------------------------------------------------- /lib/runners/schematic.runner.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from './abstract.runner'; 2 | 3 | export class SchematicRunner extends AbstractRunner { 4 | constructor() { 5 | super(`node`, [`"${SchematicRunner.findClosestSchematicsBinary()}"`]); 6 | } 7 | 8 | public static getModulePaths() { 9 | return module.paths; 10 | } 11 | 12 | public static findClosestSchematicsBinary(): string { 13 | try { 14 | return require.resolve( 15 | '@angular-devkit/schematics-cli/bin/schematics.js', 16 | { paths: this.getModulePaths() }, 17 | ); 18 | } catch { 19 | throw new Error("'schematics' binary path could not be found!"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/runners/yarn.runner.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from './abstract.runner'; 2 | 3 | export class YarnRunner extends AbstractRunner { 4 | constructor() { 5 | super('yarn'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/schematics/abstract.collection.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from '../runners'; 2 | import { Schematic } from './nest.collection'; 3 | import { SchematicOption } from './schematic.option'; 4 | 5 | export abstract class AbstractCollection { 6 | constructor( 7 | protected collection: string, 8 | protected runner: AbstractRunner, 9 | ) {} 10 | 11 | public async execute( 12 | name: string, 13 | options: SchematicOption[], 14 | extraFlags?: string, 15 | ) { 16 | let command = this.buildCommandLine(name, options); 17 | command = extraFlags ? command.concat(` ${extraFlags}`) : command; 18 | await this.runner.run(command); 19 | } 20 | 21 | public abstract getSchematics(): Schematic[]; 22 | 23 | private buildCommandLine(name: string, options: SchematicOption[]): string { 24 | return `${this.collection}:${name}${this.buildOptions(options)}`; 25 | } 26 | 27 | private buildOptions(options: SchematicOption[]): string { 28 | return options.reduce((line, option) => { 29 | return line.concat(` ${option.toCommandString()}`); 30 | }, ''); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/schematics/collection.factory.ts: -------------------------------------------------------------------------------- 1 | import { Runner, RunnerFactory } from '../runners'; 2 | import { SchematicRunner } from '../runners/schematic.runner'; 3 | import { AbstractCollection } from './abstract.collection'; 4 | import { Collection } from './collection'; 5 | import { CustomCollection } from './custom.collection'; 6 | import { NestCollection } from './nest.collection'; 7 | 8 | export class CollectionFactory { 9 | public static create(collection: Collection | string): AbstractCollection { 10 | const schematicRunner = RunnerFactory.create( 11 | Runner.SCHEMATIC, 12 | ) as SchematicRunner; 13 | 14 | if (collection === Collection.NESTJS) { 15 | return new NestCollection(schematicRunner); 16 | } else { 17 | return new CustomCollection(collection, schematicRunner); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/schematics/collection.ts: -------------------------------------------------------------------------------- 1 | export enum Collection { 2 | NESTJS = '@nestjs/schematics', 3 | } 4 | -------------------------------------------------------------------------------- /lib/schematics/custom.collection.ts: -------------------------------------------------------------------------------- 1 | import { NodeWorkflow } from '@angular-devkit/schematics/tools'; 2 | import { AbstractCollection } from './abstract.collection'; 3 | import { Schematic } from './nest.collection'; 4 | 5 | export interface CollectionSchematic { 6 | schema: string; 7 | description: string; 8 | aliases: string[]; 9 | } 10 | 11 | export class CustomCollection extends AbstractCollection { 12 | public getSchematics(): Schematic[] { 13 | const workflow = new NodeWorkflow(process.cwd(), {}); 14 | const collection = workflow.engine.createCollection(this.collection); 15 | const collectionDescriptions = [ 16 | collection.description, 17 | ...(collection.baseDescriptions ?? []), 18 | ]; 19 | const usedNames = new Set(); 20 | const schematics: Schematic[] = []; 21 | for (const collectionDesc of collectionDescriptions) { 22 | const schematicsDescs = Object.entries(collectionDesc.schematics); 23 | for (const [name, { description, aliases = [] }] of schematicsDescs) { 24 | if (usedNames.has(name)) { 25 | continue; 26 | } 27 | usedNames.add(name); 28 | const alias = aliases.find((a) => !usedNames.has(a)) ?? name; 29 | for (const alias of aliases) { 30 | usedNames.add(alias); 31 | } 32 | schematics.push({ name, alias, description }); 33 | } 34 | } 35 | return schematics.sort((a, b) => 36 | a.name < b.name ? -1 : a.name > b.name ? 1 : 0, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/schematics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collection'; 2 | export * from './collection.factory'; 3 | export * from './schematic.option'; 4 | export * from './abstract.collection'; 5 | -------------------------------------------------------------------------------- /lib/schematics/nest.collection.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from '../runners'; 2 | import { AbstractCollection } from './abstract.collection'; 3 | import { SchematicOption } from './schematic.option'; 4 | 5 | export interface Schematic { 6 | name: string; 7 | alias: string; 8 | description: string; 9 | } 10 | 11 | export class NestCollection extends AbstractCollection { 12 | private static schematics: Schematic[] = [ 13 | { 14 | name: 'application', 15 | alias: 'application', 16 | description: 'Generate a new application workspace', 17 | }, 18 | { 19 | name: 'angular-app', 20 | alias: 'ng-app', 21 | description: '', 22 | }, 23 | { 24 | name: 'class', 25 | alias: 'cl', 26 | description: 'Generate a new class', 27 | }, 28 | { 29 | name: 'configuration', 30 | alias: 'config', 31 | description: 'Generate a CLI configuration file', 32 | }, 33 | { 34 | name: 'controller', 35 | alias: 'co', 36 | description: 'Generate a controller declaration', 37 | }, 38 | { 39 | name: 'decorator', 40 | alias: 'd', 41 | description: 'Generate a custom decorator', 42 | }, 43 | { 44 | name: 'filter', 45 | alias: 'f', 46 | description: 'Generate a filter declaration', 47 | }, 48 | { 49 | name: 'gateway', 50 | alias: 'ga', 51 | description: 'Generate a gateway declaration', 52 | }, 53 | { 54 | name: 'guard', 55 | alias: 'gu', 56 | description: 'Generate a guard declaration', 57 | }, 58 | { 59 | name: 'interceptor', 60 | alias: 'itc', 61 | description: 'Generate an interceptor declaration', 62 | }, 63 | { 64 | name: 'interface', 65 | alias: 'itf', 66 | description: 'Generate an interface', 67 | }, 68 | { 69 | name: 'library', 70 | alias: 'lib', 71 | description: 'Generate a new library within a monorepo', 72 | }, 73 | { 74 | name: 'middleware', 75 | alias: 'mi', 76 | description: 'Generate a middleware declaration', 77 | }, 78 | { 79 | name: 'module', 80 | alias: 'mo', 81 | description: 'Generate a module declaration', 82 | }, 83 | { 84 | name: 'pipe', 85 | alias: 'pi', 86 | description: 'Generate a pipe declaration', 87 | }, 88 | { 89 | name: 'provider', 90 | alias: 'pr', 91 | description: 'Generate a provider declaration', 92 | }, 93 | { 94 | name: 'resolver', 95 | alias: 'r', 96 | description: 'Generate a GraphQL resolver declaration', 97 | }, 98 | { 99 | name: 'resource', 100 | alias: 'res', 101 | description: 'Generate a new CRUD resource', 102 | }, 103 | { 104 | name: 'service', 105 | alias: 's', 106 | description: 'Generate a service declaration', 107 | }, 108 | { 109 | name: 'sub-app', 110 | alias: 'app', 111 | description: 'Generate a new application within a monorepo', 112 | }, 113 | ]; 114 | 115 | constructor(runner: AbstractRunner) { 116 | super('@nestjs/schematics', runner); 117 | } 118 | 119 | public async execute(name: string, options: SchematicOption[]) { 120 | const schematic: string = this.validate(name); 121 | await super.execute(schematic, options); 122 | } 123 | 124 | public getSchematics(): Schematic[] { 125 | return NestCollection.schematics.filter( 126 | (item) => item.name !== 'angular-app', 127 | ); 128 | } 129 | 130 | private validate(name: string) { 131 | const schematic = NestCollection.schematics.find( 132 | (s) => s.name === name || s.alias === name, 133 | ); 134 | 135 | if (schematic === undefined || schematic === null) { 136 | throw new Error( 137 | `Invalid schematic "${name}". Please, ensure that "${name}" exists in this collection.`, 138 | ); 139 | } 140 | return schematic.name; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/schematics/schematic.option.ts: -------------------------------------------------------------------------------- 1 | import { normalizeToKebabOrSnakeCase } from '../utils/formatting'; 2 | 3 | export class SchematicOption { 4 | constructor( 5 | private name: string, 6 | private value: boolean | string, 7 | ) {} 8 | 9 | get normalizedName() { 10 | return normalizeToKebabOrSnakeCase(this.name); 11 | } 12 | 13 | public toCommandString(): string { 14 | if (typeof this.value === 'string') { 15 | if (this.name === 'name') { 16 | return `--${this.normalizedName}=${this.format()}`; 17 | } else if (this.name === 'version' || this.name === 'path') { 18 | return `--${this.normalizedName}=${this.value}`; 19 | } else { 20 | return `--${this.normalizedName}="${this.value}"`; 21 | } 22 | } else if (typeof this.value === 'boolean') { 23 | const str = this.normalizedName; 24 | return this.value ? `--${str}` : `--no-${str}`; 25 | } else { 26 | return `--${this.normalizedName}=${this.value}`; 27 | } 28 | } 29 | 30 | private format() { 31 | return normalizeToKebabOrSnakeCase(this.value as string) 32 | .split('') 33 | .reduce((content, char) => { 34 | if (char === '(' || char === ')' || char === '[' || char === ']') { 35 | return `${content}\\${char}`; 36 | } 37 | return `${content}${char}`; 38 | }, ''); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/banner.ts: -------------------------------------------------------------------------------- 1 | export const BANNER = ` 2 | _ _ _ ___ _____ _____ _ _____ 3 | | \\ | | | | |_ |/ ___|/ __ \\| | |_ _| 4 | | \\| | ___ ___ | |_ | |\\ \`--. | / \\/| | | | 5 | | . \` | / _ \\/ __|| __| | | \`--. \\| | | | | | 6 | | |\\ || __/\\__ \\| |_ /\\__/ //\\__/ /| \\__/\\| |_____| |_ 7 | \\_| \\_/ \\___||___/ \\__|\\____/ \\____/ \\____/\\_____/\\___/ 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /lib/ui/emojis.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'node-emoji'; 2 | 3 | export const EMOJIS = { 4 | HEART: get('heart'), 5 | COFFEE: get('coffee'), 6 | BEER: get('beer'), 7 | BROKEN_HEART: get('broken_heart'), 8 | CRYING: get('crying_cat_face'), 9 | HEART_EYES: get('heart_eyes_cat'), 10 | JOY: get('joy_cat'), 11 | KISSING: get('kissing_cat'), 12 | SCREAM: get('scream_cat'), 13 | ROCKET: get('rocket'), 14 | SMIRK: get('smirk_cat'), 15 | RAISED_HANDS: get('raised_hands'), 16 | POINT_RIGHT: get('point_right'), 17 | SPARKLES: get('sparkles'), 18 | BOOM: get('boom'), 19 | PRAY: get('pray'), 20 | WINE: get('wine_glass'), 21 | }; 22 | -------------------------------------------------------------------------------- /lib/ui/errors.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-line-length 2 | 3 | export const CLI_ERRORS = { 4 | MISSING_TYPESCRIPT: (path: string) => 5 | `Could not find TypeScript configuration file "${path}". Please, ensure that you are running this command in the appropriate directory (inside Nest workspace).`, 6 | WRONG_PLUGIN: (name: string) => 7 | `The "${name}" plugin is not compatible with Nest CLI. Neither "after()" nor "before()" nor "afterDeclarations()" function have been provided.`, 8 | }; 9 | -------------------------------------------------------------------------------- /lib/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './banner'; 2 | export * from './emojis'; 3 | export * from './errors'; 4 | export * from './messages'; 5 | export * from './prefixes'; 6 | -------------------------------------------------------------------------------- /lib/ui/messages.ts: -------------------------------------------------------------------------------- 1 | import { green } from 'ansis'; 2 | import { EMOJIS } from './emojis'; 3 | 4 | export const MESSAGES = { 5 | PROJECT_NAME_QUESTION: 'What name would you like to use for the new project?', 6 | PROJECT_SELECTION_QUESTION: 'Which project would you like to generate to?', 7 | LIBRARY_PROJECT_SELECTION_QUESTION: 8 | 'Which project would you like to add the library to?', 9 | DRY_RUN_MODE: 'Command has been executed in dry run mode, nothing changed!', 10 | PROJECT_INFORMATION_START: `${EMOJIS.SPARKLES} We will scaffold your app in a few seconds..`, 11 | RUNNER_EXECUTION_ERROR: (command: string) => 12 | `\nFailed to execute command: ${command}`, 13 | PACKAGE_MANAGER_QUESTION: `Which package manager would you ${EMOJIS.HEART} to use?`, 14 | PACKAGE_MANAGER_INSTALLATION_IN_PROGRESS: `Installation in progress... ${EMOJIS.COFFEE}`, 15 | PACKAGE_MANAGER_UPDATE_IN_PROGRESS: `Installation in progress... ${EMOJIS.COFFEE}`, 16 | PACKAGE_MANAGER_UPGRADE_IN_PROGRESS: `Installation in progress... ${EMOJIS.COFFEE}`, 17 | PACKAGE_MANAGER_PRODUCTION_INSTALLATION_IN_PROGRESS: `Package installation in progress... ${EMOJIS.COFFEE}`, 18 | GIT_INITIALIZATION_ERROR: 'Git repository has not been initialized', 19 | PACKAGE_MANAGER_INSTALLATION_SUCCEED: (name: string) => 20 | name !== '.' 21 | ? `${EMOJIS.ROCKET} Successfully created project ${green(name)}` 22 | : `${EMOJIS.ROCKET} Successfully created a new project`, 23 | GET_STARTED_INFORMATION: `${EMOJIS.POINT_RIGHT} Get started with the following commands:`, 24 | CHANGE_DIR_COMMAND: (name: string) => `$ cd ${name}`, 25 | START_COMMAND: (name: string) => `$ ${name} run start`, 26 | PACKAGE_MANAGER_INSTALLATION_FAILED: (commandToRunManually: string) => 27 | `${EMOJIS.SCREAM} Packages installation failed!\nIn case you don't see any errors above, consider manually running the failed command ${commandToRunManually} to see more details on why it errored out.`, 28 | // tslint:disable-next-line:max-line-length 29 | NEST_INFORMATION_PACKAGE_MANAGER_FAILED: `${EMOJIS.SMIRK} cannot read your project package.json file, are you inside your project directory?`, 30 | NEST_INFORMATION_PACKAGE_WARNING_FAILED: (nestDependencies: string[]) => 31 | `${ 32 | EMOJIS.SMIRK 33 | } failed to compare dependencies versions, please check that following packages are in the same minor version : \n ${nestDependencies.join( 34 | '\n', 35 | )}`, 36 | 37 | LIBRARY_INSTALLATION_FAILED_BAD_PACKAGE: (name: string) => 38 | `Unable to install library ${name} because package did not install. Please check package name.`, 39 | LIBRARY_INSTALLATION_FAILED_NO_LIBRARY: 'No library found.', 40 | LIBRARY_INSTALLATION_STARTS: 'Starting library setup...', 41 | }; 42 | -------------------------------------------------------------------------------- /lib/ui/prefixes.ts: -------------------------------------------------------------------------------- 1 | import { bgRgb } from 'ansis'; 2 | 3 | export const ERROR_PREFIX = bgRgb(210, 0, 75).bold.rgb(0, 0, 0)( 4 | ' Error ', 5 | ); 6 | export const INFO_PREFIX = bgRgb(60, 190, 100).bold.rgb(0, 0, 0)( 7 | ' Info ', 8 | ); 9 | -------------------------------------------------------------------------------- /lib/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param str 4 | * @returns formated string 5 | * @description normalizes input to supported path and file name format. 6 | * Changes camelCase strings to kebab-case, replaces spaces with dash and keeps underscores. 7 | */ 8 | export function normalizeToKebabOrSnakeCase(str: string) { 9 | const STRING_DASHERIZE_REGEXP = /\s/g; 10 | const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; 11 | return str 12 | .replace(STRING_DECAMELIZE_REGEXP, '$1-$2') 13 | .toLowerCase() 14 | .replace(STRING_DASHERIZE_REGEXP, '-'); 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/get-default-tsconfig-path.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { join } from 'path'; 3 | 4 | const TSCONFIG_BUILD_JSON = 'tsconfig.build.json'; 5 | const TSCONFIG_JSON = 'tsconfig.json'; 6 | 7 | export function getDefaultTsconfigPath() { 8 | return fs.existsSync(join(process.cwd(), TSCONFIG_BUILD_JSON)) 9 | ? TSCONFIG_BUILD_JSON 10 | : TSCONFIG_JSON; 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/gracefully-exit-on-prompt-error.ts: -------------------------------------------------------------------------------- 1 | export function gracefullyExitOnPromptError(err: Error) { 2 | if (err.name === 'ExitPromptError') { 3 | process.exit(1); 4 | } else { 5 | throw err; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils/is-module-available.ts: -------------------------------------------------------------------------------- 1 | export function isModuleAvailable(path: string): boolean { 2 | try { 3 | require.resolve(path); 4 | return true; 5 | } catch { 6 | return false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/load-configuration.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ConfigurationLoader } from '../configuration'; 2 | import { NestConfigurationLoader } from '../configuration/nest-configuration.loader'; 3 | import { FileSystemReader } from '../readers'; 4 | 5 | export async function loadConfiguration(): Promise> { 6 | const loader: ConfigurationLoader = new NestConfigurationLoader( 7 | new FileSystemReader(process.cwd()), 8 | ); 9 | return loader.load(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/local-binaries.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join, posix } from 'path'; 3 | import { CommandLoader } from '../../commands'; 4 | 5 | const localBinPathSegments = [process.cwd(), 'node_modules', '@nestjs', 'cli']; 6 | 7 | export function localBinExists() { 8 | return existsSync(join(...localBinPathSegments)); 9 | } 10 | 11 | export function loadLocalBinCommandLoader(): typeof CommandLoader { 12 | // eslint-disable-next-line @typescript-eslint/no-var-requires 13 | const commandsFile = require(posix.join(...localBinPathSegments, 'commands')); 14 | return commandsFile.CommandLoader; 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/os-info.utils.ts: -------------------------------------------------------------------------------- 1 | export default function osName(platform: string, release: string): string { 2 | switch (platform) { 3 | case 'darwin': 4 | return Number(release.split('.')[0]) > 15 ? 'macOS' : 'OS X'; 5 | case 'linux': 6 | return 'Linux'; 7 | case 'win32': 8 | return 'Windows'; 9 | case 'freebsd': 10 | return 'FreeBSD'; 11 | case 'openbsd': 12 | return 'OpenBSD'; 13 | case 'sunos': 14 | return 'Solaris'; 15 | case 'android': 16 | return 'Android'; 17 | default: 18 | return platform; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/project-utils.ts: -------------------------------------------------------------------------------- 1 | import { select } from '@inquirer/prompts'; 2 | import { Input } from '../../commands'; 3 | import { getValueOrDefault } from '../compiler/helpers/get-value-or-default'; 4 | import { Configuration, ProjectConfiguration } from '../configuration'; 5 | import { generateSelect } from '../questions/questions'; 6 | import { gracefullyExitOnPromptError } from './gracefully-exit-on-prompt-error'; 7 | 8 | export function shouldAskForProject( 9 | schematic: string, 10 | configurationProjects: { [key: string]: ProjectConfiguration }, 11 | appName: string, 12 | ) { 13 | return ( 14 | ['app', 'sub-app', 'library', 'lib'].includes(schematic) === false && 15 | configurationProjects && 16 | Object.entries(configurationProjects).length !== 0 && 17 | !appName 18 | ); 19 | } 20 | 21 | export function shouldGenerateSpec( 22 | configuration: Required, 23 | schematic: string, 24 | appName: string, 25 | specValue: boolean, 26 | specPassedAsInput?: boolean, 27 | ) { 28 | if (specPassedAsInput === true || specPassedAsInput === undefined) { 29 | // CLI parameters has the highest priority 30 | return specValue; 31 | } 32 | 33 | let specConfiguration = getValueOrDefault( 34 | configuration, 35 | 'generateOptions.spec', 36 | appName || '', 37 | ); 38 | if (typeof specConfiguration === 'boolean') { 39 | return specConfiguration; 40 | } 41 | 42 | if ( 43 | typeof specConfiguration === 'object' && 44 | specConfiguration[schematic] !== undefined 45 | ) { 46 | return specConfiguration[schematic]; 47 | } 48 | 49 | if (typeof specConfiguration === 'object' && appName) { 50 | // The appName has a generateOption spec, but not for the schematic trying to generate 51 | // Check if the global generateOptions has a spec to use instead 52 | specConfiguration = getValueOrDefault( 53 | configuration, 54 | 'generateOptions.spec', 55 | '', 56 | ); 57 | if (typeof specConfiguration === 'boolean') { 58 | return specConfiguration; 59 | } 60 | 61 | if ( 62 | typeof specConfiguration === 'object' && 63 | specConfiguration[schematic] !== undefined 64 | ) { 65 | return specConfiguration[schematic]; 66 | } 67 | } 68 | return specValue; 69 | } 70 | 71 | export function shouldGenerateFlat( 72 | configuration: Required, 73 | appName: string, 74 | flatValue: boolean, 75 | ): boolean { 76 | // CLI parameters have the highest priority 77 | if (flatValue === true) { 78 | return flatValue; 79 | } 80 | 81 | const flatConfiguration = getValueOrDefault( 82 | configuration, 83 | 'generateOptions.flat', 84 | appName || '', 85 | ); 86 | if (typeof flatConfiguration === 'boolean') { 87 | return flatConfiguration; 88 | } 89 | return flatValue; 90 | } 91 | 92 | export function getSpecFileSuffix( 93 | configuration: Required, 94 | appName: string, 95 | specFileSuffixValue: string, 96 | ): string { 97 | // CLI parameters have the highest priority 98 | if (specFileSuffixValue) { 99 | return specFileSuffixValue; 100 | } 101 | 102 | const specFileSuffixConfiguration = getValueOrDefault( 103 | configuration, 104 | 'generateOptions.specFileSuffix', 105 | appName || '', 106 | undefined, 107 | undefined, 108 | 'spec', 109 | ); 110 | if (typeof specFileSuffixConfiguration === 'string') { 111 | return specFileSuffixConfiguration; 112 | } 113 | return specFileSuffixValue; 114 | } 115 | 116 | export async function askForProjectName( 117 | promptQuestion: string, 118 | projects: string[], 119 | ) { 120 | const projectNameSelect = generateSelect('appName')(promptQuestion)(projects); 121 | return select(projectNameSelect).catch(gracefullyExitOnPromptError); 122 | } 123 | 124 | export function moveDefaultProjectToStart( 125 | configuration: Configuration, 126 | defaultProjectName: string, 127 | defaultLabel: string, 128 | ) { 129 | let projects: string[] = 130 | configuration.projects != null ? Object.keys(configuration.projects) : []; 131 | if (configuration.sourceRoot !== 'src') { 132 | projects = projects.filter( 133 | (p) => p !== defaultProjectName.replace(defaultLabel, ''), 134 | ); 135 | } 136 | projects.unshift(defaultProjectName); 137 | return projects; 138 | } 139 | 140 | export function hasValidOptionFlag( 141 | queriedOptionName: string, 142 | options: Input[], 143 | queriedValue: string | number | boolean = true, 144 | ): boolean { 145 | return options.some( 146 | (option: Input) => 147 | option.name === queriedOptionName && option.value === queriedValue, 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /lib/utils/remaining-flags.ts: -------------------------------------------------------------------------------- 1 | import { CommanderStatic } from 'commander'; 2 | 3 | export function getRemainingFlags(cli: CommanderStatic) { 4 | const rawArgs = [...cli.rawArgs]; 5 | return rawArgs 6 | .splice( 7 | Math.max( 8 | rawArgs.findIndex((item: string) => item.startsWith('--')), 9 | 0, 10 | ), 11 | ) 12 | .filter((item: string, index: number, array: string[]) => { 13 | // If the option is consumed by commander.js, then we skip it 14 | if (cli.options.find((o: any) => o.short === item || o.long === item)) { 15 | return false; 16 | } 17 | 18 | // If it's an argument of an option consumed by commander.js, then we 19 | // skip it too 20 | const prevKeyRaw = array[index - 1]; 21 | if (prevKeyRaw) { 22 | const previousKey = camelCase( 23 | prevKeyRaw.replace(/--/g, '').replace('no', ''), 24 | ); 25 | if (cli[previousKey] === item) { 26 | return false; 27 | } 28 | } 29 | 30 | return true; 31 | }); 32 | } 33 | 34 | /** 35 | * Camel-case the given `flag` 36 | * 37 | * @param {String} flag 38 | * @return {String} 39 | * @api private 40 | */ 41 | 42 | function camelCase(flag: string) { 43 | return flag.split('-').reduce((str, word) => { 44 | return str + word[0].toUpperCase() + word.slice(1); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/utils/tree-kill.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | export function treeKillSync(pid: number, signal?: string | number): void { 4 | if (process.platform === 'win32') { 5 | execSync('taskkill /pid ' + pid + ' /T /F'); 6 | return; 7 | } 8 | 9 | const childs = getAllChilds(pid); 10 | childs.forEach(function (pid) { 11 | killPid(pid, signal); 12 | }); 13 | 14 | killPid(pid, signal); 15 | return; 16 | } 17 | 18 | function getAllPid(): { 19 | pid: number; 20 | ppid: number; 21 | }[] { 22 | const rows = execSync('ps -A -o pid,ppid') 23 | .toString() 24 | .trim() 25 | .split('\n') 26 | .slice(1); 27 | 28 | return rows 29 | .map(function (row) { 30 | const parts = row.match(/\s*(\d+)\s*(\d+)/); 31 | 32 | if (parts === null) { 33 | return null; 34 | } 35 | 36 | return { 37 | pid: Number(parts[1]), 38 | ppid: Number(parts[2]), 39 | }; 40 | }) 41 | .filter((input: null | undefined | T): input is T => { 42 | return input != null; 43 | }); 44 | } 45 | 46 | function getAllChilds(pid: number) { 47 | const allpid = getAllPid(); 48 | 49 | const ppidHash: { 50 | [key: number]: number[]; 51 | } = {}; 52 | 53 | const result: number[] = []; 54 | 55 | allpid.forEach(function (item) { 56 | ppidHash[item.ppid] = ppidHash[item.ppid] || []; 57 | ppidHash[item.ppid].push(item.pid); 58 | }); 59 | 60 | const find = function (pid: number) { 61 | ppidHash[pid] = ppidHash[pid] || []; 62 | ppidHash[pid].forEach(function (childPid) { 63 | result.push(childPid); 64 | find(childPid); 65 | }); 66 | }; 67 | 68 | find(pid); 69 | return result; 70 | } 71 | 72 | function killPid(pid: number, signal?: string | number) { 73 | try { 74 | process.kill(pid, signal); 75 | } catch (err) { 76 | if (err.code !== 'ESRCH') { 77 | throw err; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/utils/type-assertions.ts: -------------------------------------------------------------------------------- 1 | export function assertNonArray( 2 | value: T, 3 | ): asserts value is Exclude { 4 | if (Array.isArray(value)) { 5 | throw new TypeError('Expected a non-array value'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nestjs/cli", 3 | "version": "11.0.7", 4 | "description": "Nest - modern, fast, powerful node.js web framework (@cli)", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "engines": { 9 | "node": ">= 20.11" 10 | }, 11 | "bin": { 12 | "nest": "bin/nest.js" 13 | }, 14 | "scripts": { 15 | "build": "tsc", 16 | "clean": "gulp clean:bundle", 17 | "format": "prettier --write \"**/*.ts\"", 18 | "lint": "eslint '{lib,commands,actions}/**/*.ts' --fix", 19 | "start": "node bin/nest.js", 20 | "prepack": "npm run build", 21 | "prepublish:next": "npm run build", 22 | "publish:next": "npm publish --access public --tag next", 23 | "prepublish:npm": "npm run build", 24 | "publish:npm": "npm publish --access public", 25 | "test": "jest --config test/jest-config.json", 26 | "test:dev": "npm run clean && jest --config test/jest-config.json --watchAll", 27 | "prerelease": "npm run build", 28 | "release": "release-it", 29 | "prepare": "husky" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/nestjs/nest-cli.git" 34 | }, 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/nestjs/nest-cli/issues" 38 | }, 39 | "homepage": "https://github.com/nestjs/nest-cli#readme", 40 | "dependencies": { 41 | "@angular-devkit/core": "19.2.8", 42 | "@angular-devkit/schematics": "19.2.8", 43 | "@angular-devkit/schematics-cli": "19.2.8", 44 | "@inquirer/prompts": "7.4.1", 45 | "@nestjs/schematics": "^11.0.1", 46 | "ansis": "3.17.0", 47 | "chokidar": "4.0.3", 48 | "cli-table3": "0.6.5", 49 | "commander": "4.1.1", 50 | "fork-ts-checker-webpack-plugin": "9.1.0", 51 | "glob": "11.0.1", 52 | "node-emoji": "1.11.0", 53 | "ora": "5.4.1", 54 | "tree-kill": "1.2.2", 55 | "tsconfig-paths": "4.2.0", 56 | "tsconfig-paths-webpack-plugin": "4.2.0", 57 | "typescript": "5.8.3", 58 | "webpack": "5.99.6", 59 | "webpack-node-externals": "3.0.0" 60 | }, 61 | "devDependencies": { 62 | "@commitlint/cli": "19.8.1", 63 | "@commitlint/config-angular": "19.8.1", 64 | "@swc/cli": "0.7.7", 65 | "@swc/core": "1.11.29", 66 | "@types/inquirer": "9.0.8", 67 | "@types/jest": "29.5.14", 68 | "@types/node": "22.15.29", 69 | "@types/node-emoji": "1.8.2", 70 | "@types/webpack-node-externals": "3.0.4", 71 | "@typescript-eslint/eslint-plugin": "8.33.1", 72 | "@typescript-eslint/parser": "8.33.1", 73 | "delete-empty": "3.0.0", 74 | "eslint": "9.28.0", 75 | "eslint-config-prettier": "10.1.5", 76 | "gulp": "5.0.1", 77 | "gulp-clean": "0.4.0", 78 | "husky": "9.1.7", 79 | "jest": "29.7.0", 80 | "lint-staged": "16.1.0", 81 | "prettier": "3.5.3", 82 | "release-it": "19.0.3", 83 | "ts-jest": "29.3.4", 84 | "ts-loader": "9.5.2", 85 | "ts-node": "10.9.2" 86 | }, 87 | "lint-staged": { 88 | "**/*.{ts,json}": [] 89 | }, 90 | "peerDependencies": { 91 | "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", 92 | "@swc/core": "^1.3.62" 93 | }, 94 | "peerDependenciesMeta": { 95 | "@swc/cli": { 96 | "optional": true 97 | }, 98 | "@swc/core": { 99 | "optional": true 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [{ 4 | "depTypeList": ["devDependencies"], 5 | "automerge": true 6 | }], 7 | "extends": [ 8 | "config:base" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/actions/info.action.spec.ts: -------------------------------------------------------------------------------- 1 | import { InfoAction } from '../../actions/info.action'; 2 | 3 | jest.mock('fs', () => ({ 4 | readFileSync: jest.fn(() => '{"version": "1.2.3"}'), 5 | })); 6 | 7 | jest.mock('../../lib/package-managers', () => ({ 8 | PackageManagerFactory: { 9 | find: jest.fn(() => ({ 10 | name: 'MockedPackageManager', 11 | version: jest.fn(() => '1.0.0'), 12 | })), 13 | }, 14 | })); 15 | 16 | describe('InfoAction', () => { 17 | let infoAction: InfoAction; 18 | 19 | beforeEach(() => { 20 | infoAction = new InfoAction(); 21 | }); 22 | 23 | describe('buildNestVersionsWarningMessage', () => { 24 | it('should return an empty object for one or zero minor versions', () => { 25 | const dependencies = [ 26 | { packageName: '@nestjs/core', name: 'core', value: '1.2.3' }, 27 | { packageName: '@nestjs/common', name: 'common', value: '1.2.4' }, 28 | ]; 29 | const result = infoAction.buildNestVersionsWarningMessage(dependencies); 30 | expect(result).toEqual({}); 31 | }); 32 | 33 | it('should return an object only with whitelisted dependencies', () => { 34 | const dependencies = [ 35 | { packageName: '@nestjs/core', name: 'core', value: '1.2.3' }, 36 | { packageName: '@nestjs/common', name: 'common', value: '1.2.4' }, 37 | { 38 | packageName: '@nestjs/schematics', 39 | name: 'schematics', 40 | value: '1.2.4', 41 | }, 42 | { 43 | packageName: '@nestjs/platform-express', 44 | name: 'platform-express', 45 | value: '1.2.4', 46 | }, 47 | { 48 | packageName: '@nestjs/platform-fastify', 49 | name: 'platform-fastify', 50 | value: '1.2.4', 51 | }, 52 | { 53 | packageName: '@nestjs/platform-socket.io', 54 | name: 'platform-socket.io', 55 | value: '1.2.4', 56 | }, 57 | { 58 | packageName: '@nestjs/platform-ws', 59 | name: 'platform-ws', 60 | value: '2.1.0', 61 | }, 62 | { 63 | packageName: '@nestjs/websockets', 64 | name: 'websockets', 65 | value: '2.1.0', 66 | }, 67 | { packageName: '@nestjs/test1', name: 'test1', value: '1.2.4' }, 68 | { packageName: '@nestjs/test2', name: 'test2', value: '1.2.4' }, 69 | ]; 70 | const result = infoAction.buildNestVersionsWarningMessage(dependencies); 71 | const expected = { 72 | '1': [ 73 | { packageName: '@nestjs/core', name: 'core', value: '1.2.3' }, 74 | { packageName: '@nestjs/common', name: 'common', value: '1.2.4' }, 75 | { 76 | packageName: '@nestjs/schematics', 77 | name: 'schematics', 78 | value: '1.2.4', 79 | }, 80 | { 81 | packageName: '@nestjs/platform-express', 82 | name: 'platform-express', 83 | value: '1.2.4', 84 | }, 85 | { 86 | packageName: '@nestjs/platform-fastify', 87 | name: 'platform-fastify', 88 | value: '1.2.4', 89 | }, 90 | { 91 | packageName: '@nestjs/platform-socket.io', 92 | name: 'platform-socket.io', 93 | value: '1.2.4', 94 | }, 95 | ], 96 | '2': [ 97 | { 98 | packageName: '@nestjs/platform-ws', 99 | name: 'platform-ws', 100 | value: '2.1.0', 101 | }, 102 | { 103 | packageName: '@nestjs/websockets', 104 | name: 'websockets', 105 | value: '2.1.0', 106 | }, 107 | ], 108 | }; 109 | expect(result).toEqual(expected); 110 | }); 111 | 112 | it('should group dependencies by minor versions and sort them in descending order', () => { 113 | const dependencies = [ 114 | { 115 | name: 'schematics', 116 | packageName: '@nestjs/schematics', 117 | value: '1.2.3', 118 | }, 119 | { 120 | name: 'platform-express', 121 | packageName: '@nestjs/platform-express', 122 | value: '1.2.4', 123 | }, 124 | { 125 | name: 'platform-fastify', 126 | packageName: '@nestjs/platform-fastify', 127 | value: '2.1.0', 128 | }, 129 | { 130 | packageName: '@nestjs/platform-socket.io', 131 | name: 'platform-socket.io', 132 | value: '1.2$$.4', 133 | }, 134 | { 135 | packageName: '@nestjs/websockets', 136 | name: 'websockets', 137 | value: '^2.*&1.0', 138 | }, 139 | { 140 | name: 'platform-socket.io', 141 | packageName: '@nestjs/platform-socket.io', 142 | value: '2.0.1', 143 | }, 144 | ]; 145 | 146 | const result = infoAction.buildNestVersionsWarningMessage(dependencies); 147 | const expected = { 148 | '2': [ 149 | { 150 | name: 'platform-fastify', 151 | packageName: '@nestjs/platform-fastify', 152 | value: '2.1.0', 153 | }, 154 | { 155 | packageName: '@nestjs/websockets', 156 | name: 'websockets', 157 | value: '^2.*&1.0', 158 | }, 159 | { 160 | name: 'platform-socket.io', 161 | packageName: '@nestjs/platform-socket.io', 162 | value: '2.0.1', 163 | }, 164 | ], 165 | '1': [ 166 | { 167 | name: 'schematics', 168 | packageName: '@nestjs/schematics', 169 | value: '1.2.3', 170 | }, 171 | { 172 | name: 'platform-express', 173 | packageName: '@nestjs/platform-express', 174 | value: '1.2.4', 175 | }, 176 | { 177 | packageName: '@nestjs/platform-socket.io', 178 | name: 'platform-socket.io', 179 | value: '1.2$$.4', 180 | }, 181 | ], 182 | }; 183 | 184 | expect(result).toEqual(expected); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /test/jest-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testRegex": ".spec.ts$", 5 | "transform": { 6 | "^.+\\.(t|j)s$": "ts-jest" 7 | }, 8 | "coverageDirectory": "../coverage", 9 | "modulePaths": ["/test/lib/schematics/fixtures"] 10 | } -------------------------------------------------------------------------------- /test/lib/compiler/hooks/__snapshots__/tsconfig-paths.hook.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`tsconfig paths hooks should remove unused imports 1`] = ` 4 | Map { 5 | "dist/foo.js" => ""use strict"; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | exports.Foo = void 0; 8 | class Foo { 9 | } 10 | exports.Foo = Foo; 11 | ", 12 | "dist/bar.js" => ""use strict"; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | exports.Bar = void 0; 15 | class Bar { 16 | } 17 | exports.Bar = Bar; 18 | ", 19 | "dist/main.js" => ""use strict"; 20 | Object.defineProperty(exports, "__esModule", { value: true }); 21 | ", 22 | } 23 | `; 24 | 25 | exports[`tsconfig paths hooks should replace path of every import using a path alias by its relative path 1`] = ` 26 | Map { 27 | "dist/foo.js" => ""use strict"; 28 | Object.defineProperty(exports, "__esModule", { value: true }); 29 | exports.Foo = void 0; 30 | class Foo { 31 | } 32 | exports.Foo = Foo; 33 | ", 34 | "dist/bar.jsx" => ""use strict"; 35 | Object.defineProperty(exports, "__esModule", { value: true }); 36 | exports.Bar = void 0; 37 | class Bar { 38 | } 39 | exports.Bar = Bar; 40 | ", 41 | "dist/baz.js" => ""use strict"; 42 | Object.defineProperty(exports, "__esModule", { value: true }); 43 | exports.Baz = void 0; 44 | class Baz { 45 | } 46 | exports.Baz = Baz; 47 | ", 48 | "dist/qux.jsx" => ""use strict"; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | exports.Qux = void 0; 51 | class Qux { 52 | } 53 | exports.Qux = Qux; 54 | ", 55 | "dist/main.js" => ""use strict"; 56 | Object.defineProperty(exports, "__esModule", { value: true }); 57 | const foo_1 = require("./foo"); 58 | const bar_1 = require("./bar"); 59 | const baz_1 = require("./baz"); 60 | const qux_1 = require("./qux"); 61 | // use the imports so they do not get eliminated 62 | console.log(foo_1.Foo); 63 | console.log(bar_1.Bar); 64 | console.log(baz_1.Baz); 65 | console.log(qux_1.Qux); 66 | ", 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/aliased-imports/src/bar.tsx: -------------------------------------------------------------------------------- 1 | export class Bar {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/aliased-imports/src/baz.js: -------------------------------------------------------------------------------- 1 | export class Baz {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/aliased-imports/src/foo.ts: -------------------------------------------------------------------------------- 1 | export class Foo {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/aliased-imports/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Foo } from '~/foo'; 2 | import { Bar } from '~/bar'; 3 | import { Baz } from '~/baz'; 4 | import { Qux } from '~/qux'; 5 | 6 | // use the imports so they do not get eliminated 7 | console.log(Foo); 8 | console.log(Bar); 9 | console.log(Baz); 10 | console.log(Qux); 11 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/aliased-imports/src/qux.jsx: -------------------------------------------------------------------------------- 1 | export class Qux {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/type-imports/src/main.ts: -------------------------------------------------------------------------------- 1 | import { TypeA } from 'src/type-a'; 2 | import type { TypeB } from 'src/type-b'; 3 | import { TypeC } from './type-c'; 4 | import type { TypeD } from './type-d'; 5 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/type-imports/src/type-a.ts: -------------------------------------------------------------------------------- 1 | export type TypeA = number; 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/type-imports/src/type-b.ts: -------------------------------------------------------------------------------- 1 | export type TypeB = number; 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/type-imports/src/type-c.ts: -------------------------------------------------------------------------------- 1 | export type TypeC = number; 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/type-imports/src/type-d.ts: -------------------------------------------------------------------------------- 1 | export type TypeD = number; 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/unused-imports/src/bar.ts: -------------------------------------------------------------------------------- 1 | export class Bar {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/unused-imports/src/foo.ts: -------------------------------------------------------------------------------- 1 | export class Foo {} 2 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/fixtures/unused-imports/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Foo } from './foo'; 2 | import { Bar } from 'src/bar'; 3 | -------------------------------------------------------------------------------- /test/lib/compiler/hooks/tsconfig-paths.hook.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | import { JsxEmit } from 'typescript'; 4 | import { tsconfigPathsBeforeHookFactory } from '../../../../lib/compiler/hooks/tsconfig-paths.hook'; 5 | 6 | function createSpec( 7 | baseUrl: string, 8 | fileNames: string[], 9 | compilerOptions?: ts.CompilerOptions, 10 | ) { 11 | const options: ts.CompilerOptions = { 12 | baseUrl, 13 | outDir: path.join(baseUrl, 'dist'), 14 | target: ts.ScriptTarget.ESNext, 15 | module: ts.ModuleKind.CommonJS, 16 | ...compilerOptions, 17 | }; 18 | 19 | const program = ts.createProgram({ 20 | rootNames: fileNames.map((name) => path.join(baseUrl, name)), 21 | options, 22 | }); 23 | const output = new Map(); 24 | const transformer = tsconfigPathsBeforeHookFactory(options); 25 | program.emit( 26 | undefined, 27 | (fileName, data) => { 28 | output.set(path.relative(baseUrl, fileName), data); 29 | }, 30 | undefined, 31 | undefined, 32 | { 33 | before: transformer ? [transformer] : [], 34 | }, 35 | ); 36 | return output; 37 | } 38 | 39 | /** 40 | * This test is temporarily skipped because it's flaky on CI. 41 | * Not yet clear why but it's not a blocker. 42 | */ 43 | describe.skip('tsconfig paths hooks', () => { 44 | it('should remove type imports', async () => { 45 | const output = createSpec(path.join(__dirname, './fixtures/type-imports'), [ 46 | 'src/main.ts', 47 | 'src/type-a.ts', 48 | 'src/type-b.ts', 49 | 'src/type-c.ts', 50 | 'src/type-d.ts', 51 | ]); 52 | output.forEach((value) => { 53 | expect(value).toEqual( 54 | `"use strict";\nObject.defineProperty(exports, "__esModule", { value: true });\n`, 55 | ); 56 | }); 57 | }); 58 | 59 | it('should remove unused imports', async () => { 60 | const output = createSpec( 61 | path.join(__dirname, './fixtures/unused-imports'), 62 | ['src/main.ts', 'src/foo.ts', 'src/bar.ts'], 63 | ); 64 | expect(output).toMatchSnapshot(); 65 | }); 66 | 67 | it('should replace path of every import using a path alias by its relative path', async () => { 68 | const output = createSpec( 69 | path.join(__dirname, './fixtures/aliased-imports'), 70 | ['src/main.ts', 'src/foo.ts', 'src/bar.ts'], 71 | { paths: { '~/*': ['./src/*'] }, jsx: JsxEmit.Preserve, allowJs: true }, 72 | ); 73 | expect(output).toMatchSnapshot(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/lib/configuration/nest-configuration.loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ConfigurationLoader } from '../../../lib/configuration'; 2 | import { NestConfigurationLoader } from '../../../lib/configuration/nest-configuration.loader'; 3 | import { Reader } from '../../../lib/readers'; 4 | 5 | describe('Nest Configuration Loader', () => { 6 | let reader: Reader; 7 | beforeAll(() => { 8 | const mock = jest.fn(); 9 | mock.mockImplementation(() => { 10 | return { 11 | readAnyOf: jest.fn(() => 12 | JSON.stringify({ 13 | language: 'ts', 14 | collection: '@nestjs/schematics', 15 | }), 16 | ), 17 | read: jest.fn(() => 18 | JSON.stringify({ 19 | language: 'ts', 20 | collection: '@nestjs/schematics', 21 | entryFile: 'secondary', 22 | }), 23 | ), 24 | }; 25 | }); 26 | reader = mock(); 27 | }); 28 | it('should call reader.readAnyOf when load taking "nest-cli.json" as preferable', async () => { 29 | const loader: ConfigurationLoader = new NestConfigurationLoader(reader); 30 | const configuration: Configuration = await loader.load(); 31 | expect(reader.readAnyOf).toHaveBeenCalledWith([ 32 | 'nest-cli.json', 33 | '.nest-cli.json', 34 | ]); 35 | expect(configuration).toEqual({ 36 | language: 'ts', 37 | collection: '@nestjs/schematics', 38 | sourceRoot: 'src', 39 | entryFile: 'main', 40 | exec: 'node', 41 | monorepo: false, 42 | projects: {}, 43 | compilerOptions: { 44 | assets: [], 45 | builder: { 46 | options: { 47 | configPath: 'tsconfig.json', 48 | }, 49 | type: 'tsc', 50 | }, 51 | plugins: [], 52 | webpack: false, 53 | manualRestart: false, 54 | }, 55 | generateOptions: {}, 56 | }); 57 | }); 58 | it('should call reader.read when load with filename', async () => { 59 | const loader: ConfigurationLoader = new NestConfigurationLoader(reader); 60 | const configuration: Configuration = await loader.load( 61 | 'nest-cli.secondary.config.json', 62 | ); 63 | expect(reader.read).toHaveBeenCalledWith('nest-cli.secondary.config.json'); 64 | expect(configuration).toEqual({ 65 | language: 'ts', 66 | collection: '@nestjs/schematics', 67 | sourceRoot: 'src', 68 | entryFile: 'secondary', 69 | exec: 'node', 70 | monorepo: false, 71 | projects: {}, 72 | compilerOptions: { 73 | assets: [], 74 | builder: { 75 | options: { 76 | configPath: 'tsconfig.json', 77 | }, 78 | type: 'tsc', 79 | }, 80 | plugins: [], 81 | webpack: false, 82 | manualRestart: false, 83 | }, 84 | generateOptions: {}, 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/lib/package-managers/npm.package-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | NpmPackageManager, 4 | PackageManagerCommands, 5 | } from '../../../lib/package-managers'; 6 | import { NpmRunner } from '../../../lib/runners/npm.runner'; 7 | 8 | jest.mock('../../../lib/runners/npm.runner'); 9 | 10 | describe('NpmPackageManager', () => { 11 | let packageManager: NpmPackageManager; 12 | beforeEach(() => { 13 | (NpmRunner as any).mockClear(); 14 | (NpmRunner as any).mockImplementation(() => { 15 | return { 16 | run: (): Promise => Promise.resolve(), 17 | }; 18 | }); 19 | packageManager = new NpmPackageManager(); 20 | }); 21 | it('should be created', () => { 22 | expect(packageManager).toBeInstanceOf(NpmPackageManager); 23 | }); 24 | it('should have the correct cli commands', () => { 25 | const expectedValues: PackageManagerCommands = { 26 | install: 'install', 27 | add: 'install', 28 | update: 'update', 29 | remove: 'uninstall', 30 | saveFlag: '--save', 31 | saveDevFlag: '--save-dev', 32 | silentFlag: '--silent', 33 | }; 34 | expect(packageManager.cli).toMatchObject(expectedValues); 35 | }); 36 | describe('install', () => { 37 | it('should use the proper command for installing', () => { 38 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 39 | const dirName = '/tmp'; 40 | const testDir = join(process.cwd(), dirName); 41 | packageManager.install(dirName, 'npm'); 42 | expect(spy).toBeCalledWith('install --silent', true, testDir); 43 | }); 44 | }); 45 | describe('addProduction', () => { 46 | it('should use the proper command for adding production dependencies', () => { 47 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 48 | const dependencies = ['@nestjs/common', '@nestjs/core']; 49 | const tag = '5.0.0'; 50 | const command = `install --save ${dependencies 51 | .map((dependency) => `${dependency}@${tag}`) 52 | .join(' ')}`; 53 | packageManager.addProduction(dependencies, tag); 54 | expect(spy).toBeCalledWith(command, true); 55 | }); 56 | }); 57 | describe('addDevelopment', () => { 58 | it('should use the proper command for adding development dependencies', () => { 59 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 60 | const dependencies = ['@nestjs/common', '@nestjs/core']; 61 | const tag = '5.0.0'; 62 | const command = `install --save-dev ${dependencies 63 | .map((dependency) => `${dependency}@${tag}`) 64 | .join(' ')}`; 65 | packageManager.addDevelopment(dependencies, tag); 66 | expect(spy).toBeCalledWith(command, true); 67 | }); 68 | }); 69 | describe('updateProduction', () => { 70 | it('should use the proper command for updating production dependencies', () => { 71 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 72 | const dependencies = ['@nestjs/common', '@nestjs/core']; 73 | const command = `update ${dependencies.join(' ')}`; 74 | packageManager.updateProduction(dependencies); 75 | expect(spy).toBeCalledWith(command, true); 76 | }); 77 | }); 78 | describe('updateDevelopment', () => { 79 | it('should use the proper command for updating development dependencies', () => { 80 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 81 | const dependencies = ['@nestjs/common', '@nestjs/core']; 82 | const command = `update ${dependencies.join(' ')}`; 83 | packageManager.updateDevelopment(dependencies); 84 | expect(spy).toBeCalledWith(command, true); 85 | }); 86 | }); 87 | describe('upgradeProduction', () => { 88 | it('should use the proper command for upgrading production dependencies', () => { 89 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 90 | const dependencies = ['@nestjs/common', '@nestjs/core']; 91 | const tag = '5.0.0'; 92 | const uninstallCommand = `uninstall --save ${dependencies.join(' ')}`; 93 | 94 | const installCommand = `install --save ${dependencies 95 | .map((dependency) => `${dependency}@${tag}`) 96 | .join(' ')}`; 97 | 98 | return packageManager.upgradeProduction(dependencies, tag).then(() => { 99 | expect(spy.mock.calls).toEqual([ 100 | [uninstallCommand, true], 101 | [installCommand, true], 102 | ]); 103 | }); 104 | }); 105 | }); 106 | describe('upgradeDevelopment', () => { 107 | it('should use the proper command for upgrading production dependencies', () => { 108 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 109 | const dependencies = ['@nestjs/common', '@nestjs/core']; 110 | const tag = '5.0.0'; 111 | const uninstallCommand = `uninstall --save-dev ${dependencies.join(' ')}`; 112 | 113 | const installCommand = `install --save-dev ${dependencies 114 | .map((dependency) => `${dependency}@${tag}`) 115 | .join(' ')}`; 116 | 117 | return packageManager.upgradeDevelopment(dependencies, tag).then(() => { 118 | expect(spy.mock.calls).toEqual([ 119 | [uninstallCommand, true], 120 | [installCommand, true], 121 | ]); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/lib/package-managers/package-manager.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { 3 | NpmPackageManager, 4 | PackageManagerFactory, 5 | PnpmPackageManager, 6 | YarnPackageManager, 7 | } from '../../../lib/package-managers'; 8 | 9 | jest.mock('fs', () => ({ 10 | promises: { 11 | readdir: jest.fn(), 12 | }, 13 | })); 14 | 15 | describe('PackageManagerFactory', () => { 16 | afterAll(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | describe('.prototype.find()', () => { 21 | it('should return NpmPackageManager when no lock file is found', async () => { 22 | (fs.promises.readdir as jest.Mock).mockResolvedValue([]); 23 | 24 | const whenPackageManager = PackageManagerFactory.find(); 25 | await expect(whenPackageManager).resolves.toBeInstanceOf( 26 | NpmPackageManager, 27 | ); 28 | }); 29 | 30 | it('should return YarnPackageManager when "yarn.lock" file is found', async () => { 31 | (fs.promises.readdir as jest.Mock).mockResolvedValue(['yarn.lock']); 32 | 33 | const whenPackageManager = PackageManagerFactory.find(); 34 | await expect(whenPackageManager).resolves.toBeInstanceOf( 35 | YarnPackageManager, 36 | ); 37 | }); 38 | 39 | it('should return PnpmPackageManager when "pnpm-lock.yaml" file is found', async () => { 40 | (fs.promises.readdir as jest.Mock).mockResolvedValue(['pnpm-lock.yaml']); 41 | 42 | const whenPackageManager = PackageManagerFactory.find(); 43 | await expect(whenPackageManager).resolves.toBeInstanceOf( 44 | PnpmPackageManager, 45 | ); 46 | }); 47 | 48 | describe('when there are all supported lock files', () => { 49 | it('should prioritize "yarn.lock" file over all the others lock files', async () => { 50 | (fs.promises.readdir as jest.Mock).mockResolvedValue([ 51 | 'pnpm-lock.yaml', 52 | 'package-lock.json', 53 | // This is intentionally the last element in this array 54 | 'yarn.lock', 55 | ]); 56 | 57 | const whenPackageManager = PackageManagerFactory.find(); 58 | await expect(whenPackageManager).resolves.toBeInstanceOf( 59 | YarnPackageManager, 60 | ); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/lib/package-managers/pnpm.package-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | PnpmPackageManager, 4 | PackageManagerCommands, 5 | } from '../../../lib/package-managers'; 6 | import { PnpmRunner } from '../../../lib/runners/pnpm.runner'; 7 | 8 | jest.mock('../../../lib/runners/pnpm.runner'); 9 | 10 | describe('PnpmPackageManager', () => { 11 | let packageManager: PnpmPackageManager; 12 | beforeEach(() => { 13 | (PnpmRunner as any).mockClear(); 14 | (PnpmRunner as any).mockImplementation(() => { 15 | return { 16 | run: (): Promise => Promise.resolve(), 17 | }; 18 | }); 19 | packageManager = new PnpmPackageManager(); 20 | }); 21 | it('should be created', () => { 22 | expect(packageManager).toBeInstanceOf(PnpmPackageManager); 23 | }); 24 | it('should have the correct cli commands', () => { 25 | const expectedValues: PackageManagerCommands = { 26 | install: 'install --strict-peer-dependencies=false', 27 | add: 'install --strict-peer-dependencies=false', 28 | update: 'update', 29 | remove: 'uninstall', 30 | saveFlag: '--save', 31 | saveDevFlag: '--save-dev', 32 | silentFlag: '--reporter=silent', 33 | }; 34 | expect(packageManager.cli).toMatchObject(expectedValues); 35 | }); 36 | describe('install', () => { 37 | it('should use the proper command for installing', () => { 38 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 39 | const dirName = '/tmp'; 40 | const testDir = join(process.cwd(), dirName); 41 | packageManager.install(dirName, 'pnpm'); 42 | expect(spy).toBeCalledWith( 43 | 'install --strict-peer-dependencies=false --reporter=silent', 44 | true, 45 | testDir, 46 | ); 47 | }); 48 | }); 49 | describe('addProduction', () => { 50 | it('should use the proper command for adding production dependencies', () => { 51 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 52 | const dependencies = ['@nestjs/common', '@nestjs/core']; 53 | const tag = '5.0.0'; 54 | const command = `install --strict-peer-dependencies=false --save ${dependencies 55 | .map((dependency) => `${dependency}@${tag}`) 56 | .join(' ')}`; 57 | packageManager.addProduction(dependencies, tag); 58 | expect(spy).toBeCalledWith(command, true); 59 | }); 60 | }); 61 | describe('addDevelopment', () => { 62 | it('should use the proper command for adding development dependencies', () => { 63 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 64 | const dependencies = ['@nestjs/common', '@nestjs/core']; 65 | const tag = '5.0.0'; 66 | const command = `install --strict-peer-dependencies=false --save-dev ${dependencies 67 | .map((dependency) => `${dependency}@${tag}`) 68 | .join(' ')}`; 69 | packageManager.addDevelopment(dependencies, tag); 70 | expect(spy).toBeCalledWith(command, true); 71 | }); 72 | }); 73 | describe('updateProduction', () => { 74 | it('should use the proper command for updating production dependencies', () => { 75 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 76 | const dependencies = ['@nestjs/common', '@nestjs/core']; 77 | const command = `update ${dependencies.join(' ')}`; 78 | packageManager.updateProduction(dependencies); 79 | expect(spy).toBeCalledWith(command, true); 80 | }); 81 | }); 82 | describe('updateDevelopment', () => { 83 | it('should use the proper command for updating development dependencies', () => { 84 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 85 | const dependencies = ['@nestjs/common', '@nestjs/core']; 86 | const command = `update ${dependencies.join(' ')}`; 87 | packageManager.updateDevelopment(dependencies); 88 | expect(spy).toBeCalledWith(command, true); 89 | }); 90 | }); 91 | describe('upgradeProduction', () => { 92 | it('should use the proper command for upgrading production dependencies', () => { 93 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 94 | const dependencies = ['@nestjs/common', '@nestjs/core']; 95 | const tag = '5.0.0'; 96 | const uninstallCommand = `uninstall --save ${dependencies.join(' ')}`; 97 | 98 | const installCommand = `install --strict-peer-dependencies=false --save ${dependencies 99 | .map((dependency) => `${dependency}@${tag}`) 100 | .join(' ')}`; 101 | 102 | return packageManager.upgradeProduction(dependencies, tag).then(() => { 103 | expect(spy.mock.calls).toEqual([ 104 | [uninstallCommand, true], 105 | [installCommand, true], 106 | ]); 107 | }); 108 | }); 109 | }); 110 | describe('upgradeDevelopment', () => { 111 | it('should use the proper command for upgrading production dependencies', () => { 112 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 113 | const dependencies = ['@nestjs/common', '@nestjs/core']; 114 | const tag = '5.0.0'; 115 | const uninstallCommand = `uninstall --save-dev ${dependencies.join(' ')}`; 116 | 117 | const installCommand = `install --strict-peer-dependencies=false --save-dev ${dependencies 118 | .map((dependency) => `${dependency}@${tag}`) 119 | .join(' ')}`; 120 | 121 | return packageManager.upgradeDevelopment(dependencies, tag).then(() => { 122 | expect(spy.mock.calls).toEqual([ 123 | [uninstallCommand, true], 124 | [installCommand, true], 125 | ]); 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/lib/package-managers/yarn.package-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | PackageManagerCommands, 4 | YarnPackageManager, 5 | } from '../../../lib/package-managers'; 6 | import { YarnRunner } from '../../../lib/runners/yarn.runner'; 7 | 8 | jest.mock('../../../lib/runners/yarn.runner'); 9 | 10 | describe('YarnPackageManager', () => { 11 | let packageManager: YarnPackageManager; 12 | beforeEach(() => { 13 | (YarnRunner as any).mockClear(); 14 | (YarnRunner as any).mockImplementation(() => { 15 | return { 16 | run: (): Promise => Promise.resolve(), 17 | }; 18 | }); 19 | packageManager = new YarnPackageManager(); 20 | }); 21 | it('should be created', () => { 22 | expect(packageManager).toBeInstanceOf(YarnPackageManager); 23 | }); 24 | it('should have the correct cli commands', () => { 25 | const expectedValues: PackageManagerCommands = { 26 | install: 'install', 27 | add: 'add', 28 | update: 'upgrade', 29 | remove: 'remove', 30 | saveFlag: '', 31 | saveDevFlag: '-D', 32 | silentFlag: '--silent', 33 | }; 34 | expect(packageManager.cli).toMatchObject(expectedValues); 35 | }); 36 | describe('install', () => { 37 | it('should use the proper command for installing', () => { 38 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 39 | const dirName = '/tmp'; 40 | const testDir = join(process.cwd(), dirName); 41 | packageManager.install(dirName, 'yarn'); 42 | expect(spy).toBeCalledWith('install --silent', true, testDir); 43 | }); 44 | }); 45 | describe('addProduction', () => { 46 | it('should use the proper command for adding production dependencies', () => { 47 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 48 | const dependencies = ['@nestjs/common', '@nestjs/core']; 49 | const tag = '5.0.0'; 50 | const command = `add ${dependencies 51 | .map((dependency) => `${dependency}@${tag}`) 52 | .join(' ')}`; 53 | packageManager.addProduction(dependencies, tag); 54 | expect(spy).toBeCalledWith(command, true); 55 | }); 56 | }); 57 | describe('addDevelopment', () => { 58 | it('should use the proper command for adding development dependencies', () => { 59 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 60 | const dependencies = ['@nestjs/common', '@nestjs/core']; 61 | const tag = '5.0.0'; 62 | const command = `add -D ${dependencies 63 | .map((dependency) => `${dependency}@${tag}`) 64 | .join(' ')}`; 65 | packageManager.addDevelopment(dependencies, tag); 66 | expect(spy).toBeCalledWith(command, true); 67 | }); 68 | }); 69 | describe('updateProduction', () => { 70 | it('should use the proper command for updating production dependencies', () => { 71 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 72 | const dependencies = ['@nestjs/common', '@nestjs/core']; 73 | const command = `upgrade ${dependencies.join(' ')}`; 74 | packageManager.updateProduction(dependencies); 75 | expect(spy).toBeCalledWith(command, true); 76 | }); 77 | }); 78 | describe('updateDevelopment', () => { 79 | it('should use the proper command for updating development dependencies', () => { 80 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 81 | const dependencies = ['@nestjs/common', '@nestjs/core']; 82 | const command = `upgrade ${dependencies.join(' ')}`; 83 | packageManager.updateDevelopment(dependencies); 84 | expect(spy).toBeCalledWith(command, true); 85 | }); 86 | }); 87 | describe('upgradeProduction', () => { 88 | it('should use the proper command for upgrading production dependencies', () => { 89 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 90 | const dependencies = ['@nestjs/common', '@nestjs/core']; 91 | const tag = '5.0.0'; 92 | const uninstallCommand = `remove ${dependencies.join(' ')}`; 93 | 94 | const installCommand = `add ${dependencies 95 | .map((dependency) => `${dependency}@${tag}`) 96 | .join(' ')}`; 97 | 98 | return packageManager.upgradeProduction(dependencies, tag).then(() => { 99 | expect(spy.mock.calls).toEqual([ 100 | [uninstallCommand, true], 101 | [installCommand, true], 102 | ]); 103 | }); 104 | }); 105 | }); 106 | describe('upgradeDevelopment', () => { 107 | it('should use the proper command for upgrading production dependencies', () => { 108 | const spy = jest.spyOn((packageManager as any).runner, 'run'); 109 | const dependencies = ['@nestjs/common', '@nestjs/core']; 110 | const tag = '5.0.0'; 111 | const uninstallCommand = `remove -D ${dependencies.join(' ')}`; 112 | 113 | const installCommand = `add -D ${dependencies 114 | .map((dependency) => `${dependency}@${tag}`) 115 | .join(' ')}`; 116 | 117 | return packageManager.upgradeDevelopment(dependencies, tag).then(() => { 118 | expect(spy.mock.calls).toEqual([ 119 | [uninstallCommand, true], 120 | [installCommand, true], 121 | ]); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/lib/questions/questions.spec.ts: -------------------------------------------------------------------------------- 1 | import { Question } from 'inquirer'; 2 | import { Input } from '../../../commands/command.input'; 3 | import { 4 | generateInput, 5 | generateSelect, 6 | } from '../../../lib/questions/questions'; 7 | 8 | describe('Questions', () => { 9 | describe('generateInput', () => { 10 | it('should return an input question', () => { 11 | const input: Input = { 12 | name: 'name', 13 | value: 'test', 14 | }; 15 | const message = 'name:'; 16 | const question: Question = generateInput(input.name, message)('name'); 17 | expect(question).toEqual({ 18 | name: 'name', 19 | message, 20 | default: 'name', 21 | }); 22 | }); 23 | }); 24 | describe('generateSelect', () => { 25 | it('should return a select question', () => { 26 | const choices: string[] = ['choiceA', 'choiceB', 'choiceC']; 27 | const question = generateSelect('name')('message')(choices); 28 | expect(question).toEqual({ 29 | message: 'message', 30 | name: 'name', 31 | choices: [ 32 | { 33 | name: 'choiceA', 34 | value: 'choiceA', 35 | }, 36 | { 37 | name: 'choiceB', 38 | value: 'choiceB', 39 | }, 40 | { 41 | name: 'choiceC', 42 | value: 'choiceC', 43 | }, 44 | ], 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/lib/readers/file-system.reader.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { FileSystemReader, Reader } from '../../../lib/readers'; 3 | 4 | jest.mock('fs', () => ({ 5 | readdirSync: jest.fn().mockResolvedValue([]), 6 | readFileSync: jest.fn().mockResolvedValue('content'), 7 | })); 8 | 9 | const dir: string = process.cwd(); 10 | const reader: Reader = new FileSystemReader(dir); 11 | 12 | describe('File System Reader', () => { 13 | afterAll(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | it('should use fs.readdirSync when list (for performance reasons)', async () => { 17 | reader.list(); 18 | expect(fs.readdirSync).toHaveBeenCalled(); 19 | }); 20 | it('should use fs.readFileSync when read (for performance reasons)', async () => { 21 | reader.read('filename'); 22 | expect(fs.readFileSync).toHaveBeenCalled(); 23 | }); 24 | 25 | describe('readAnyOf tests', () => { 26 | it('should call readFileSync when running readAnyOf fn', async () => { 27 | const filenames: string[] = ['file1', 'file2', 'file3']; 28 | reader.readAnyOf(filenames); 29 | 30 | expect(fs.readFileSync).toHaveBeenCalled(); 31 | }); 32 | 33 | it('should return undefined when no file is passed', async () => { 34 | const content = reader.readAnyOf([]); 35 | expect(content).toEqual(undefined); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/lib/runners/schematic.runner.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | const withSep = (route: string) => 4 | path.resolve(route.split('/').join(path.sep)); 5 | 6 | const existsSyncTrueForPathMock = (pathToExist: string) => { 7 | pathToExist = withSep(pathToExist); 8 | return (pathToCheck: string) => pathToCheck === pathToExist; 9 | }; 10 | 11 | const getModulePathsMock = (fullPath: string) => { 12 | const moduleLoaderPaths: string[] = []; 13 | const fullPathBits = fullPath.split(path.sep); 14 | 15 | while (--fullPathBits.length !== 0) { 16 | if (fullPathBits[fullPathBits.length - 1] === 'node_modules') { 17 | continue; 18 | } 19 | 20 | const modulePath = [...fullPathBits, 'node_modules'].join(path.sep); 21 | 22 | moduleLoaderPaths.push(modulePath); 23 | } 24 | 25 | return () => moduleLoaderPaths; 26 | }; 27 | 28 | describe('SchematicRunner', () => { 29 | describe('Mocking Checks', () => { 30 | describe('existsSyncTrueForPathMock', () => { 31 | it('it should return true only for one specific path', () => { 32 | const truePath = withSep('/this/path/exists'); 33 | const wrongPath = withSep('/this/path/doesnt/exist'); 34 | const existsSync = existsSyncTrueForPathMock(truePath); 35 | 36 | expect(existsSync(truePath)).toBe(true); 37 | expect(existsSync(wrongPath)).toBe(false); 38 | }); 39 | }); 40 | 41 | describe('getModulePathsMock', () => { 42 | it('it should return the same array as the module.paths module-local field', () => { 43 | const realModulePath = module.paths; 44 | const mockedModulePath = getModulePathsMock(__filename)(); 45 | 46 | expect(mockedModulePath).toEqual(realModulePath); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/lib/schematics/custom.collection.spec.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { AbstractRunner } from '../../../lib/runners'; 3 | import { CustomCollection } from '../../../lib/schematics/custom.collection'; 4 | 5 | describe('Custom Collection', () => { 6 | it(`should list schematics from simple collection`, async () => { 7 | const mock = jest.fn(); 8 | mock.mockImplementation(() => { 9 | return { 10 | logger: {}, 11 | run: jest.fn().mockImplementation(() => Promise.resolve()), 12 | }; 13 | }); 14 | const mockedRunner = mock(); 15 | const collection = new CustomCollection( 16 | require.resolve('./fixtures/simple/collection.json'), 17 | mockedRunner as AbstractRunner, 18 | ); 19 | const schematics = collection.getSchematics(); 20 | expect(schematics).toEqual([ 21 | { name: 'simple1', alias: 's1', description: 'Simple schematic 1' }, 22 | { name: 'simple2', alias: 's2', description: 'Simple schematic 2' }, 23 | { name: 'simple3', alias: 's3', description: 'Simple schematic 3' }, 24 | ]); 25 | }); 26 | 27 | it(`should list schematics from extended collection`, async () => { 28 | const mock = jest.fn(); 29 | mock.mockImplementation(() => { 30 | return { 31 | logger: {}, 32 | run: jest.fn().mockImplementation(() => Promise.resolve()), 33 | }; 34 | }); 35 | const mockedRunner = mock(); 36 | const collection = new CustomCollection( 37 | require.resolve('./fixtures/extended/collection.json'), 38 | mockedRunner as AbstractRunner, 39 | ); 40 | const schematics = collection.getSchematics(); 41 | expect(schematics).toEqual([ 42 | { name: 'extend1', alias: 'x1', description: 'Extended schematic 1' }, 43 | { name: 'extend2', alias: 'x2', description: 'Extended schematic 2' }, 44 | { name: 'simple1', alias: 's1', description: 'Override schematic 1' }, 45 | { 46 | name: 'simple2', 47 | alias: 'os2', 48 | description: 'Override schematic 2', 49 | }, 50 | { 51 | name: 'simple3', 52 | alias: 'simple3', 53 | description: 'Simple schematic 3', 54 | }, 55 | ]); 56 | }); 57 | 58 | it(`should list schematics from package with collection.json path in package.json`, async () => { 59 | const mock = jest.fn(); 60 | mock.mockImplementation(() => { 61 | return { 62 | logger: {}, 63 | run: jest.fn().mockImplementation(() => Promise.resolve()), 64 | }; 65 | }); 66 | const mockedRunner = mock(); 67 | const collection = new CustomCollection( 68 | 'package', 69 | mockedRunner as AbstractRunner, 70 | ); 71 | const schematics = collection.getSchematics(); 72 | expect(schematics).toEqual([ 73 | { name: 'package1', alias: 'pkg1', description: 'Package schematic 1' }, 74 | ]); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/lib/schematics/fixtures/extended/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "extends": ["../simple/collection.json"], 4 | "schematics": { 5 | "simple1": { 6 | "factory": "factory", 7 | "description": "Override schematic 1", 8 | "aliases": ["s1", "simp1"] 9 | }, 10 | "simple2": { 11 | "factory": "factory", 12 | "description": "Override schematic 2", 13 | "aliases": ["os2", "s2", "simp2"] 14 | }, 15 | "extend1": { 16 | "factory": "factory", 17 | "description": "Extended schematic 1", 18 | "aliases": ["x1", "ext1"] 19 | }, 20 | "extend2": { 21 | "factory": "factory", 22 | "description": "Extended schematic 2", 23 | "aliases": ["x2", "ext2", "s3", "simp3"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/lib/schematics/fixtures/package/a/b/c/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../../../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "package1": { 5 | "factory": "factory", 6 | "description": "Package schematic 1", 7 | "aliases": ["pkg1"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/lib/schematics/fixtures/package/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestjs/nest-cli/673a10ddcec7cf70ee6cbcd7d84b2173b21221ce/test/lib/schematics/fixtures/package/index.js -------------------------------------------------------------------------------- /test/lib/schematics/fixtures/package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package", 3 | "version": "0.0.0", 4 | "schematics": "./a/b/c/collection.json" 5 | } 6 | -------------------------------------------------------------------------------- /test/lib/schematics/fixtures/simple/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "simple1": { 5 | "factory": "factory", 6 | "description": "Simple schematic 1", 7 | "aliases": ["s1", "simp1"] 8 | }, 9 | "simple2": { 10 | "factory": "factory", 11 | "description": "Simple schematic 2", 12 | "aliases": ["s2", "simp2"] 13 | }, 14 | "simple3": { 15 | "factory": "factory", 16 | "description": "Simple schematic 3", 17 | "aliases": ["s3", "simp3"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/lib/schematics/nest.collection.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRunner } from '../../../lib/runners'; 2 | import { NestCollection } from '../../../lib/schematics/nest.collection'; 3 | 4 | describe('Nest Collection', () => { 5 | [ 6 | 'application', 7 | 'class', 8 | 'configuration', 9 | 'controller', 10 | 'decorator', 11 | 'library', 12 | 'filter', 13 | 'gateway', 14 | 'guard', 15 | 'interceptor', 16 | 'interface', 17 | 'middleware', 18 | 'module', 19 | 'pipe', 20 | 'provider', 21 | 'resolver', 22 | 'service', 23 | 'sub-app', 24 | 'resource', 25 | ].forEach((schematic) => { 26 | it(`should call runner with ${schematic} schematic name`, async () => { 27 | const mock = jest.fn(); 28 | mock.mockImplementation(() => { 29 | return { 30 | logger: {}, 31 | run: jest.fn().mockImplementation(() => Promise.resolve()), 32 | }; 33 | }); 34 | const mockedRunner = mock(); 35 | const collection = new NestCollection(mockedRunner as AbstractRunner); 36 | await collection.execute(schematic, []); 37 | expect(mockedRunner.run).toHaveBeenCalledWith( 38 | `@nestjs/schematics:${schematic}`, 39 | ); 40 | }); 41 | }); 42 | [ 43 | { name: 'application', alias: 'application' }, 44 | { name: 'class', alias: 'cl' }, 45 | { name: 'configuration', alias: 'config' }, 46 | { name: 'controller', alias: 'co' }, 47 | { name: 'decorator', alias: 'd' }, 48 | { name: 'library', alias: 'lib' }, 49 | { name: 'filter', alias: 'f' }, 50 | { name: 'gateway', alias: 'ga' }, 51 | { name: 'guard', alias: 'gu' }, 52 | { name: 'interceptor', alias: 'itc' }, 53 | { name: 'interface', alias: 'itf' }, 54 | { name: 'middleware', alias: 'mi' }, 55 | { name: 'module', alias: 'mo' }, 56 | { name: 'pipe', alias: 'pi' }, 57 | { name: 'provider', alias: 'pr' }, 58 | { name: 'resolver', alias: 'r' }, 59 | { name: 'service', alias: 's' }, 60 | { name: 'sub-app', alias: 'app' }, 61 | { name: 'resource', alias: 'res' }, 62 | ].forEach((schematic) => { 63 | it(`should call runner with schematic ${schematic.name} name when use ${schematic.alias} alias`, async () => { 64 | const mock = jest.fn(); 65 | mock.mockImplementation(() => { 66 | return { 67 | logger: {}, 68 | run: jest.fn().mockImplementation(() => Promise.resolve()), 69 | }; 70 | }); 71 | const mockedRunner = mock(); 72 | const collection = new NestCollection(mockedRunner as AbstractRunner); 73 | await collection.execute(schematic.alias, []); 74 | expect(mockedRunner.run).toHaveBeenCalledWith( 75 | `@nestjs/schematics:${schematic.name}`, 76 | ); 77 | }); 78 | }); 79 | it('should throw an error when schematic name is not in nest collection', async () => { 80 | const mock = jest.fn(); 81 | mock.mockImplementation(() => { 82 | return { 83 | logger: {}, 84 | run: jest.fn().mockImplementation(() => Promise.resolve()), 85 | }; 86 | }); 87 | const mockedRunner = mock(); 88 | const collection = new NestCollection(mockedRunner as AbstractRunner); 89 | try { 90 | await collection.execute('name', []); 91 | } catch (error) { 92 | expect(error.message).toEqual( 93 | 'Invalid schematic "name". Please, ensure that "name" exists in this collection.', 94 | ); 95 | } 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/lib/schematics/schematic.option.spec.ts: -------------------------------------------------------------------------------- 1 | import { SchematicOption } from '../../../lib/schematics'; 2 | 3 | interface TestOption { 4 | input: string; 5 | expected: string; 6 | } 7 | 8 | interface TestFlag { 9 | input: boolean; 10 | } 11 | 12 | type TestSuite = { 13 | description: string; 14 | option: string; 15 | } & (TestOption | TestFlag); 16 | 17 | function isFlagTest(test: any): test is TestFlag { 18 | return typeof test.expected === 'undefined'; 19 | } 20 | 21 | describe('Schematic Option', () => { 22 | const tests: TestSuite[] = [ 23 | { 24 | description: 'should manage string option name', 25 | option: 'name', 26 | input: 'my-app', 27 | expected: 'my-app', 28 | }, 29 | { 30 | description: 'should manage spaced string option value name', 31 | option: 'name', 32 | input: 'my app', 33 | expected: 'my-app', 34 | }, 35 | { 36 | description: 'should manage camelcased string option value name', 37 | option: 'name', 38 | input: 'myApp', 39 | expected: 'my-app', 40 | }, 41 | { 42 | description: 'should allow underscore string option value name', 43 | option: 'name', 44 | input: 'my_app', 45 | expected: 'my_app', 46 | }, 47 | { 48 | description: 'should manage classified string option value name', 49 | option: 'name', 50 | input: 'MyApp', 51 | expected: 'my-app', 52 | }, 53 | { 54 | description: 'should manage parenthesis string option value name', 55 | option: 'name', 56 | input: 'my-(app)', 57 | expected: 'my-\\(app\\)', 58 | }, 59 | { 60 | description: 'should manage brackets string option value name', 61 | option: 'name', 62 | input: 'my-[app]', 63 | expected: 'my-\\[app\\]', 64 | }, 65 | { 66 | description: 'should manage description', 67 | option: 'description', 68 | input: 'My super app', 69 | expected: '"My super app"', 70 | }, 71 | { 72 | description: 'should manage author with special chars', 73 | option: 'author', 74 | input: 'name ', 75 | expected: '"name "', 76 | }, 77 | { 78 | description: 'should use "strict" mode', 79 | option: 'strict', 80 | input: true, 81 | }, 82 | { 83 | description: 'should not use "strict" mode', 84 | option: 'strict', 85 | input: false, 86 | }, 87 | { 88 | description: 'should manage version', 89 | option: 'version', 90 | input: '1.0.0', 91 | expected: '1.0.0', 92 | }, 93 | { 94 | description: 'should manage version', 95 | option: 'path', 96 | input: 'path/to/generate', 97 | expected: 'path/to/generate', 98 | }, 99 | ]; 100 | 101 | tests.forEach((test) => { 102 | it(test.description, () => { 103 | const option = new SchematicOption(test.option, test.input); 104 | 105 | if (isFlagTest(test)) { 106 | if (test.input) { 107 | expect(option.toCommandString()).toEqual(`--${test.option}`); 108 | } else { 109 | expect(option.toCommandString()).toEqual(`--no-${test.option}`); 110 | } 111 | } else { 112 | expect(option.toCommandString()).toEqual( 113 | `--${test.option}=${test.expected}`, 114 | ); 115 | } 116 | }); 117 | }); 118 | 119 | it('should manage boolean option', () => { 120 | const option = new SchematicOption('dry-run', false); 121 | expect(option.toCommandString()).toEqual('--no-dry-run'); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/lib/utils/get-default-tsconfig-path.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { getDefaultTsconfigPath } from '../../../lib/utils/get-default-tsconfig-path'; 3 | 4 | jest.mock('fs', () => { 5 | return { 6 | existsSync: jest.fn(), 7 | }; 8 | }); 9 | 10 | describe('getDefaultTsconfigPath', () => { 11 | afterAll(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | it('should get tsconfig.json when tsconfig.build.json not exist', () => { 15 | jest.spyOn(fs, 'existsSync').mockReturnValue(false); 16 | const result = getDefaultTsconfigPath(); 17 | expect(result).toBe('tsconfig.json'); 18 | }); 19 | it('should get tsconfig.build.json when tsconfig.build.json exist', () => { 20 | jest.spyOn(fs, 'existsSync').mockReturnValue(true); 21 | const result = getDefaultTsconfigPath(); 22 | expect(result).toBe('tsconfig.build.json'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tools/gulp/config.ts: -------------------------------------------------------------------------------- 1 | // All paths are related to the base dir 2 | export const sources = ['lib', 'actions', 'commands', 'bin']; 3 | -------------------------------------------------------------------------------- /tools/gulp/gulpfile.ts: -------------------------------------------------------------------------------- 1 | import './tasks/clean'; 2 | -------------------------------------------------------------------------------- /tools/gulp/tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import * as deleteEmpty from 'delete-empty'; 2 | import { series, src, task } from 'gulp'; 3 | import * as clean from 'gulp-clean'; 4 | import { sources } from '../config'; 5 | 6 | /** 7 | * Cleans the build output assets from the packages folders 8 | */ 9 | function cleanOutput() { 10 | const files = sources.map((source) => [ 11 | `${source}/**/*.js`, 12 | `${source}/**/*.d.ts`, 13 | `${source}/**/*.js.map`, 14 | `${source}/**/*.d.ts.map`, 15 | ]); 16 | return src( 17 | files.reduce((a, b) => a.concat(b), []), 18 | { 19 | read: false, 20 | }, 21 | ).pipe(clean()); 22 | } 23 | 24 | /** 25 | * Cleans empty dirs 26 | */ 27 | function cleanDirs(done: () => void) { 28 | sources.forEach((source) => deleteEmpty.sync(`${source}/`)); 29 | done(); 30 | } 31 | 32 | task('clean:output', cleanOutput); 33 | task('clean:dirs', cleanDirs); 34 | task('clean:bundle', series('clean:output', 'clean:dirs')); 35 | -------------------------------------------------------------------------------- /tools/gulp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "noUnusedParameters": false, 5 | "noUnusedLocals": false, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "../../dist/tools/gulp", 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noImplicitThis": true, 12 | "noEmitOnError": true, 13 | "noImplicitAny": false, 14 | "target": "ES2021", 15 | "types": [ 16 | "node" 17 | ], 18 | "typeRoots": ["./typings", "../../node_modules/@types/"], 19 | "baseUrl": ".", 20 | }, 21 | "files": [ 22 | "gulpfile.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tools/gulp/util/task-helpers.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | function isDirectory(path: string) { 5 | return statSync(path).isDirectory(); 6 | } 7 | 8 | export function getFolders(dir: string) { 9 | return readdirSync(dir).filter((file) => isDirectory(join(dir, file))); 10 | } 11 | 12 | export function getDirs(base: string) { 13 | return getFolders(base).map((path) => `${base}/${path}`); 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "target": "ES2021", 6 | "skipLibCheck": true, 7 | "sourceMap": false, 8 | "allowJs": true, 9 | "strict": true, 10 | "useUnknownInCatchVariables": false, 11 | "types": [ 12 | "node", 13 | "jest" 14 | ] 15 | }, 16 | "include": ["actions", "bin", "commands", "lib"], 17 | "exclude": ["node_modules", "e2e"] 18 | } 19 | --------------------------------------------------------------------------------