├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── pre-push ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ └── cli-integration.test.ts ├── assets ├── demo.gif └── demo.yml ├── bin └── lt ├── docs ├── commands.md └── plugins.md ├── eslint.config.mjs ├── extras ├── prettier-imports.js └── sync-version.mjs ├── package-lock.json ├── package.json ├── src ├── cli.ts ├── commands │ ├── blocks │ │ ├── add.ts │ │ └── blocks.ts │ ├── cli │ │ ├── cli.ts │ │ ├── create.ts │ │ └── rename.ts │ ├── components │ │ ├── add.ts │ │ └── components.ts │ ├── deployment │ │ ├── create.ts │ │ └── deployment.ts │ ├── docs │ │ ├── docs.ts │ │ └── open.ts │ ├── frontend │ │ ├── angular.ts │ │ ├── frontend.ts │ │ └── nuxt.ts │ ├── fullstack │ │ ├── fullstack.ts │ │ └── init.ts │ ├── git │ │ ├── clean.ts │ │ ├── clear.ts │ │ ├── create.ts │ │ ├── force-pull.ts │ │ ├── get.ts │ │ ├── git.ts │ │ ├── rebase.ts │ │ ├── rename.ts │ │ ├── reset.ts │ │ ├── squash.ts │ │ ├── undo.ts │ │ └── update.ts │ ├── lt.ts │ ├── npm │ │ ├── npm.ts │ │ ├── reinit.ts │ │ └── update.ts │ ├── server │ │ ├── add-property.ts │ │ ├── create-secret.ts │ │ ├── create.ts │ │ ├── module.ts │ │ ├── object.ts │ │ ├── server.ts │ │ ├── set-secrets.ts │ │ └── test.ts │ ├── starter │ │ ├── chrome-extension.ts │ │ └── starter.ts │ ├── tools │ │ ├── crypt.ts │ │ ├── jwt-read.ts │ │ ├── regex.ts │ │ ├── sha256.ts │ │ └── tools.ts │ ├── typescript │ │ ├── create.ts │ │ ├── playground.ts │ │ └── typescript.ts │ └── update.ts ├── extensions │ ├── git.ts │ ├── server.ts │ ├── tools.ts │ └── typescript.ts ├── interfaces │ ├── ServerProps.interface.ts │ ├── extended-gluegun-command.ts │ └── extended-gluegun-toolbox.ts ├── lt.config.js └── templates │ ├── deployment │ ├── .github │ │ └── workflows │ │ │ ├── pre-release.yml.ejs │ │ │ └── release.yml.ejs │ ├── .gitlab-ci.yml.ejs │ ├── Dockerfile.app.ejs │ ├── Dockerfile.ejs │ ├── docker-compose.dev.yml.ejs │ ├── docker-compose.prod.yml.ejs │ ├── docker-compose.test.yml.ejs │ └── scripts │ │ ├── build-push.sh.ejs │ │ └── deploy.sh.ejs │ ├── model.ts.ejs │ ├── monorepro │ └── README.md.ejs │ ├── nest-server-module │ ├── inputs │ │ ├── template-create.input.ts.ejs │ │ └── template.input.ts.ejs │ ├── outputs │ │ └── template-fac-result.output.ts.ejs │ ├── template.controller.ts.ejs │ ├── template.model.ts.ejs │ ├── template.module.ts.ejs │ ├── template.resolver.ts.ejs │ └── template.service.ts.ejs │ ├── nest-server-object │ ├── template-create.input.ts.ejs │ ├── template.input.ts.ejs │ └── template.object.ts.ejs │ ├── nest-server-starter │ └── README.md.ejs │ ├── nest-server-tests │ └── tests.e2e-spec.ts.ejs │ └── typescript-starter │ └── README.md.ejs └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for CI via CircleCI 2 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 3 | version: 2 4 | 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version you desire here 9 | - image: circleci/node:16.12 10 | 11 | # Specify service dependencies here if necessary 12 | # CircleCI maintains a library of pre-built images 13 | # documented at https://circleci.com/docs/2.0/circleci-images/ 14 | # - image: circleci/mongo:3.4.4 15 | 16 | working_directory: ~/repo 17 | 18 | steps: 19 | - checkout 20 | 21 | # Download and cache dependencies 22 | - restore_cache: 23 | keys: 24 | - v1-dependencies-{{ checksum "package.json" }} 25 | # fallback to using the latest cache if no exact match is found 26 | - v1-dependencies- 27 | 28 | - run: npm install 29 | 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: v1-dependencies-{{ checksum "package.json" }} 34 | 35 | # run build (with linting and tests before) 36 | - run: npm run build 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # all files 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # Markdown files 16 | [*.md] 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lenne.tech/ts", 3 | "rules": { 4 | "@typescript-eslint/brace-style": ["error", "1tbs", {"allowSingleLine": false}], 5 | "@typescript-eslint/member-delimiter-style": [ 6 | "error", 7 | { 8 | "multiline": { 9 | "delimiter": "semi", 10 | "requireLast": true 11 | }, 12 | "singleline": { 13 | "delimiter": "semi", 14 | "requireLast": false 15 | }, 16 | "multilineDetection": "brackets" 17 | } 18 | ], 19 | "@typescript-eslint/no-require-imports": "off", 20 | "@typescript-eslint/no-var-requires": "off", 21 | "curly": ["error", "all"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*/*' 7 | - '*' 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Git checkout 15 | uses: actions/checkout@v4 16 | - name: Use Node.js 20 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Lint 23 | run: npm run lint 24 | - name: Build 25 | run: npm run build 26 | - name: Save build 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: build 30 | path: ./build 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Git checkout 13 | uses: actions/checkout@v4 14 | - name: Node 20 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - name: NPM install 19 | run: npm install 20 | - name: Build 21 | run: npm run build 22 | - name: Publish 23 | uses: JS-DevTools/npm-publish@v3 24 | with: 25 | token: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # uniTools 2 | dist 3 | typedocs 4 | !public/ 5 | public/* 6 | !public/images 7 | schema.gql 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | 74 | # IDEs and editors 75 | .idea 76 | .project 77 | .classpath 78 | .c9/ 79 | *.launch 80 | .settings/ 81 | *.sublime-workspace 82 | 83 | # IDE - VSCode 84 | .vscode 85 | 86 | globalConfig.json 87 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node extras/sync-version.mjs && npm run lint --fix 4 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint --fix && npm run test 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "plugins": [ 4 | "./extras/prettier-imports" 5 | ], 6 | "printWidth": 120, 7 | "semi": true, 8 | "singleQuote": true 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.0.116](https://github.com/lenneTech/cli/compare/v0.0.115...v0.0.116) (2025-04-16) 6 | 7 | 8 | ### Features 9 | 10 | * **DEV-126:** Add option for rest controller. ([#62](https://github.com/lenneTech/cli/issues/62)) ([3c50594](https://github.com/lenneTech/cli/commit/3c505949ec5f6ac616abc6917aaa8ff6747da0af)) 11 | 12 | ### [0.0.115](https://github.com/lenneTech/cli/compare/v0.0.114...v0.0.115) (2025-04-09) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **DEV-44:** update config.env.ts to replace secret keys and adjust project directory prefix ([095719c](https://github.com/lenneTech/cli/commit/095719cd9a59d0266f6f1b8e62d038d5e55b49e9)) 18 | 19 | ### [0.0.114](https://github.com/lenneTech/cli/compare/v0.0.113...v0.0.114) (2025-02-19) 20 | 21 | 22 | ### Features 23 | 24 | * **DEV-102:** Add command for blocks ([#60](https://github.com/lenneTech/cli/issues/60)) ([0e28774](https://github.com/lenneTech/cli/commit/0e28774b6cd60da36c63e3a453e1cedc7781528c)) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * **TEC-20:** Windows Scripts, nuxt command. ([#59](https://github.com/lenneTech/cli/issues/59)) ([3197e90](https://github.com/lenneTech/cli/commit/3197e900d6db0b58d333b064986e3ab6122f9e5b)) 30 | 31 | ### [0.0.113](https://github.com/lenneTech/cli/compare/v0.0.112...v0.0.113) (2024-10-15) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * fix wrong folder name ([c26c061](https://github.com/lenneTech/cli/commit/c26c0619adfd5563b689022c4ce53e70e199cf42)) 37 | 38 | ### [0.0.112](https://github.com/lenneTech/cli/compare/v0.0.111...v0.0.112) (2024-10-15) 39 | 40 | 41 | ### Features 42 | 43 | * optimize components add command ([b019666](https://github.com/lenneTech/cli/commit/b019666178568d854961374b7c4ace6d782f15a1)) 44 | * optimize components add command ([7c9046c](https://github.com/lenneTech/cli/commit/7c9046c53697d06ad3e52979fd4d87e4c07a3d0f)) 45 | 46 | ### [0.0.111](https://github.com/lenneTech/cli/compare/v0.0.110...v0.0.111) (2024-10-15) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * fix components add command ([9537965](https://github.com/lenneTech/cli/commit/9537965704de2b5597a937906f61c6c4a4c3209d)) 52 | 53 | ### [0.0.106](https://github.com/lenneTech/cli/compare/v0.0.105...v0.0.106) (2024-10-01) 54 | 55 | ### [0.0.105](https://github.com/lenneTech/cli/compare/v0.0.104...v0.0.105) (2024-07-25) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * fix creation of folder in components add command ([2e1baa9](https://github.com/lenneTech/cli/commit/2e1baa9df3751cc6ef7f44b21535784631bb666a)) 61 | 62 | ### [0.0.104](https://github.com/lenneTech/cli/compare/v0.0.103...v0.0.104) (2024-07-25) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * fix creation of folder in components add command ([d3eb7b3](https://github.com/lenneTech/cli/commit/d3eb7b39778504b214b66410111343bbf9244988)) 68 | * fix creation of folder in components add command ([c6d9350](https://github.com/lenneTech/cli/commit/c6d9350349715e4504cf6f346bfc63b787e23118)) 69 | 70 | ### [0.0.103](https://github.com/lenneTech/cli/compare/v0.0.102...v0.0.103) (2024-07-25) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * fix creation of folder in components add command ([3135344](https://github.com/lenneTech/cli/commit/3135344d5850411ae797afb132d836d404bf48d4)) 76 | 77 | ### [0.0.102](https://github.com/lenneTech/cli/compare/v0.0.101...v0.0.102) (2024-02-27) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * fix creation of folder in components add command ([7616b2d](https://github.com/lenneTech/cli/commit/7616b2d7674385c6377b440db4b589e5d2c3ad09)) 83 | 84 | ### [0.0.101](https://github.com/lenneTech/cli/compare/v0.0.99...v0.0.101) (2024-02-20) 85 | 86 | ### [0.0.94](https://github.com/lenneTech/cli/compare/v0.0.92...v0.0.94) (2023-08-19) 87 | 88 | ### [0.0.92](https://github.com/lenneTech/cli/compare/v0.0.91...v0.0.92) (2023-07-03) 89 | 90 | ### Bug Fixes 91 | 92 | - fix fullstack init ([9a42035](https://github.com/lenneTech/cli/commit/9a42035c383ba967629457c8802024f9c86ff7ab)) 93 | 94 | ### [0.0.91](https://github.com/lenneTech/cli/compare/v0.0.90...v0.0.91) (2023-07-03) 95 | 96 | ### Bug Fixes 97 | 98 | - rename fullstack ([d687c0e](https://github.com/lenneTech/cli/commit/d687c0e29d0a27811f787a94d7ce24ea6fc4d2f0)) 99 | 100 | ### [0.0.90](https://github.com/lenneTech/cli/compare/v0.0.89...v0.0.90) (2023-07-03) 101 | 102 | ### Bug Fixes 103 | 104 | - fix fullstack command ([7c2c3fa](https://github.com/lenneTech/cli/commit/7c2c3fae38bfb35fcb43bf2bcc52213827f457ce)) 105 | 106 | ### [0.0.89](https://github.com/lenneTech/cli/compare/v0.0.88...v0.0.89) (2023-07-03) 107 | 108 | ### Features 109 | 110 | - Add fullstack init command ([55bd4dc](https://github.com/lenneTech/cli/commit/55bd4dca270f066feeafc06b4e8e0ee708e25fc8)) 111 | 112 | ### [0.0.84](https://github.com/lenneTech/cli/compare/v0.0.83...v0.0.84) (2022-11-25) 113 | 114 | ### Features 115 | 116 | - Optimized SSR configuration for angular projects ([491d0e2](https://github.com/lenneTech/cli/commit/491d0e24cad736462d69f2a7737522b2f8b1fa39)) 117 | 118 | ### [0.0.83](https://github.com/lenneTech/cli/compare/v0.0.82...v0.0.83) (2022-11-24) 119 | 120 | ### Features 121 | 122 | - Optimized creating Fullstack and CI/CD handling ([b38c1c9](https://github.com/lenneTech/cli/commit/b38c1c9c9e2cd3d822736b08bf1f7b32719ff1fe)) 123 | 124 | ### [0.0.82](https://github.com/lenneTech/cli/compare/v0.0.80...v0.0.82) (2022-11-15) 125 | 126 | ### [0.0.80](https://github.com/lenneTech/cli/compare/v0.0.79...v0.0.80) (2022-11-09) 127 | 128 | ### Features 129 | 130 | - Add generator for deployment ([e2d1a0c](https://github.com/lenneTech/cli/commit/e2d1a0c297bbca3437d4d2c5adbcf6f23ba27e97)) 131 | 132 | ### [0.0.72](https://github.com/lenneTech/cli/compare/v0.0.71...v0.0.72) (2022-05-26) 133 | 134 | ### Bug Fixes 135 | 136 | - Add missing templates in build directory ([39465d8](https://github.com/lenneTech/cli/commit/39465d8f041f5a6ee594cdc6647e97becd78594f)) 137 | 138 | ### [0.0.71](https://github.com/lenneTech/cli/compare/v0.0.70...v0.0.71) (2022-05-22) 139 | 140 | ### Features 141 | 142 | - Optimized module creation ([b9aa303](https://github.com/lenneTech/cli/commit/b9aa303e757445d18d98668a0ce582b5c1be4c3f)) 143 | 144 | ### [0.0.70](https://github.com/lenneTech/cli/compare/v0.0.69...v0.0.70) (2022-05-17) 145 | 146 | ### Features 147 | 148 | - Update packages ([da7fe71](https://github.com/lenneTech/cli/commit/da7fe71d83471b65f3d1620a8274396491a9f75d)) 149 | 150 | ### [0.0.69](https://github.com/lenneTech/cli/compare/v0.0.63...v0.0.69) (2022-05-17) 151 | 152 | ### Features 153 | 154 | - Update angular-create command to new ng-base-starter and lt-monorepo ([c4da520](https://github.com/lenneTech/cli/commit/c4da52012e983f5d05a7ec9997c5958b8f639b73)) 155 | 156 | ### [0.0.63](https://github.com/lenneTech/cli/compare/v0.0.62...v0.0.63) (2021-10-21) 157 | 158 | ### Features 159 | 160 | - Adjust nest-server-module for subscriptions ([60da49f](https://github.com/lenneTech/cli/commit/60da49fa9d7acd25a5c16399674da8869a6b4286)) 161 | 162 | ### [0.0.62](https://github.com/lenneTech/cli/compare/v0.0.53...v0.0.62) (2021-10-21) 163 | 164 | ### Features 165 | 166 | - Add standard-version ([9b78f63](https://github.com/lenneTech/cli/commit/9b78f638136b6fbc8fbc16961d261c6bec28ca25)) 167 | 168 | ### Bug Fixes 169 | 170 | - Fix wrong imports and map object after update; Closes [#41](https://github.com/lenneTech/cli/issues/41) Closes [#42](https://github.com/lenneTech/cli/issues/42) ([4aa6cf9](https://github.com/lenneTech/cli/commit/4aa6cf91a583b070e1c3db8c83bc33272c19dde8)) 171 | - Remove dependencies overwrite ([27df608](https://github.com/lenneTech/cli/commit/27df6085e04cd506466bc298355033fe23e5c5e7)) 172 | - Rename master to main ([653b24c](https://github.com/lenneTech/cli/commit/653b24cbebed230af9a80f4da29974fedc3ccc83)) 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 lenne.Tech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lt CLI 2 | 3 | A CLI for [lenne.Tech](https://github.com/lenneTech) libraries and frameworks. 4 | 5 | CLI in action: 6 | 7 | ![Gluegun Menu Demo](assets/demo.gif) 8 | 9 | [![License](https://img.shields.io/github/license/lenneTech/cli)](/LICENSE) [![CircleCI](https://circleci.com/gh/lenneTech/cli/tree/master.svg?style=shield)](https://circleci.com/gh/lenneTech/cli/tree/master) 10 | [![Dependency Status](https://david-dm.org/lenneTech/cli.svg)](https://david-dm.org/lenneTech/cli) [![devDependency Status](https://david-dm.org/lenneTech/cli/dev-status.svg)](https://david-dm.org/lenneTech/cli?type=dev) 11 | 12 | 15 | 16 | ## Installation 17 | 18 | ``` 19 | $ npm install -g @lenne.tech/cli 20 | ``` 21 | 22 | ## Usage 23 | 24 | ``` 25 | Menu mode 26 | $ lt 27 | or command line mode 28 | $ lt () () 29 | ``` 30 | 31 | ## Help / List of commands 32 | 33 | ``` 34 | $ lt help 35 | or 36 | $ lt 37 | ``` 38 | 39 | ## Examples 40 | 41 | ``` 42 | // Start 43 | $ lt 44 | 45 | // Create new server 46 | $ lt server create 47 | or 48 | $ lt server c 49 | 50 | // Create new module for server (in server project root dir) 51 | $ lt server module 52 | or 53 | $ lt server m 54 | 55 | // Update and install npm packages (in project dir) 56 | $ lt npm update 57 | or 58 | $ lt npm up 59 | or 60 | $ lt npm u 61 | 62 | // Checkout git branch and update packages (in project dir) 63 | $ lt git get 64 | or 65 | $ lt git g 66 | 67 | ... 68 | 69 | ``` 70 | 71 | ## Development 72 | 73 | ``` 74 | # Clone project 75 | git clone git@github.com:lenneTech/cli.git 76 | cd cli 77 | 78 | # Link the project for global usage 79 | npm link 80 | 81 | # Make changes 82 | ... 83 | 84 | # Test changes 85 | lt ... 86 | 87 | # Build new version 88 | npm build 89 | ``` 90 | 91 | ## Thanks 92 | 93 | Many thanks to the developers of [Glugun](https://infinitered.github.io/gluegun) 94 | and all the developers whose packages are used here. 95 | 96 | ## License 97 | 98 | MIT - see LICENSE 99 | -------------------------------------------------------------------------------- /__tests__/cli-integration.test.ts: -------------------------------------------------------------------------------- 1 | import * as config from '../package.json'; 2 | 3 | const { system, filesystem } = require('gluegun'); 4 | 5 | const src = filesystem.path(__dirname, '..'); 6 | 7 | const cli = async cmd => 8 | system.run('node ' + filesystem.path(src, 'bin', 'lt') + ` ${cmd}`); 9 | 10 | test('outputs version', async () => { 11 | const output = await cli('--version'); 12 | expect(output).toContain(config.version); 13 | }); 14 | 15 | test('outputs help', async () => { 16 | const output = await cli('--help'); 17 | expect(output).toContain(config.version); 18 | }); 19 | 20 | /* 21 | test('generates file', async () => { 22 | const output = await cli('generate foo') 23 | 24 | expect(output).toContain('Generated file at models/foo-model.ts') 25 | const foomodel = filesystem.read('models/foo-model.ts') 26 | 27 | expect(foomodel).toContain(`module.exports = {`) 28 | expect(foomodel).toContain(`name: 'foo'`) 29 | 30 | // cleanup artifact 31 | filesystem.remove('models') 32 | }) 33 | */ 34 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lenneTech/cli/d982403db88f2294f18304d76d82c6a0a83414cf/assets/demo.gif -------------------------------------------------------------------------------- /bin/lt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | /* tslint:disable */ 5 | // check if we're running in dev mode 6 | var devMode = require('fs').existsSync(`${__dirname}/../src`); 7 | // or want to "force" running the compiled version with --compiled-build 8 | var wantsCompiled = process.argv.indexOf('--compiled-build') >= 0; 9 | 10 | if (wantsCompiled || !devMode) { 11 | // this runs from the compiled javascript source 12 | require(`${__dirname}/../build/cli`).run(process.argv); 13 | } else { 14 | // this runs from the typescript source (for dev only) 15 | // hook into ts-node so we can run typescript on the fly 16 | require('ts-node').register({ project: `${__dirname}/../tsconfig.json` }); 17 | // run the CLI with the current process arguments 18 | require(`${__dirname}/../src/cli`).run(process.argv); 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Command Reference for lt 2 | 3 | TODO: Add your command reference here 4 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin guide for lt 2 | 3 | Plugins allow you to add features to lt, such as commands and 4 | extensions to the `toolbox` object that provides the majority of the functionality 5 | used by lt. 6 | 7 | Creating a lt plugin is easy. Just create a repo with two folders: 8 | 9 | ``` 10 | commands/ 11 | extensions/ 12 | ``` 13 | 14 | A command is a file that looks something like this: 15 | 16 | ```js 17 | // commands/foo.js 18 | 19 | module.exports = { 20 | run: (toolbox) => { 21 | const { print, filesystem } = toolbox 22 | 23 | const desktopDirectories = filesystem.subdirectories(`~/Desktop`) 24 | print.info(desktopDirectories) 25 | } 26 | } 27 | ``` 28 | 29 | An extension lets you add additional features to the `toolbox`. 30 | 31 | ```js 32 | // extensions/bar-extension.js 33 | 34 | module.exports = (toolbox) => { 35 | const { print } = toolbox 36 | 37 | toolbox.bar = () => { print.info('Bar!') } 38 | } 39 | ``` 40 | 41 | This is then accessible in your plugin's commands as `toolbox.bar`. 42 | 43 | # Loading a plugin 44 | 45 | To load a particular plugin (which has to start with `lt-*`), 46 | install it to your project using `npm install --save-dev lt-PLUGINNAME`, 47 | and lt will pick it up automatically. 48 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@lenne.tech/eslint-config-ts' 2 | 3 | export default [ 4 | ...typescript, 5 | { 6 | rules: { 7 | "unused-imports/no-unused-vars": [ 8 | "warn", 9 | { 10 | "caughtErrors": "none" 11 | }, 12 | ], 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /extras/prettier-imports.js: -------------------------------------------------------------------------------- 1 | const { parsers: typescriptParsers } = require('prettier/parser-typescript'); 2 | const ts = require('typescript'); 3 | 4 | // ============================================================================= 5 | // Prettier plugin to optimize and sort imports 6 | // see https://github.com/prettier/prettier/issues/6260 7 | // ============================================================================= 8 | 9 | class SingleLanguageServiceHost { 10 | constructor(name, content) { 11 | this.name = name; 12 | this.content = content; 13 | this.getCompilationSettings = ts.getDefaultCompilerOptions; 14 | this.getDefaultLibFileName = ts.getDefaultLibFilePath; 15 | } 16 | 17 | getScriptFileNames() { 18 | return [this.name]; 19 | } 20 | 21 | getScriptVersion() { 22 | return ts.version; 23 | } 24 | 25 | getScriptSnapshot() { 26 | return ts.ScriptSnapshot.fromString(this.content); 27 | } 28 | 29 | getCurrentDirectory() { 30 | return ''; 31 | } 32 | } 33 | 34 | function applyChanges(text, changes) { 35 | return changes.reduceRight((text, change) => { 36 | const head = text.slice(0, change.span.start); 37 | const tail = text.slice(change.span.start + change.span.length); 38 | return `${head}${change.newText}${tail}`; 39 | }, text); 40 | } 41 | 42 | function organizeImports(text) { 43 | const fileName = 'file.ts'; 44 | const host = new SingleLanguageServiceHost(fileName, text); 45 | const languageService = ts.createLanguageService(host); 46 | const formatOptions = ts.getDefaultFormatCodeSettings(); 47 | const fileChanges = languageService.organizeImports( 48 | { type: 'file', fileName }, 49 | formatOptions, 50 | {} 51 | ); 52 | const textChanges = [...fileChanges.map(change => change.textChanges)]; 53 | return applyChanges(text, textChanges); 54 | } 55 | 56 | const parsers = { 57 | typescript: { 58 | ...typescriptParsers.typescript, 59 | preprocess(text) { 60 | text = organizeImports(text); 61 | return text; 62 | } 63 | } 64 | }; 65 | 66 | // Uses module.export because of 'Unexpected token export' error 67 | module.exports = parsers; 68 | -------------------------------------------------------------------------------- /extras/sync-version.mjs: -------------------------------------------------------------------------------- 1 | import pkg from '@lenne.tech/npm-package-helper'; 2 | const NpmPackageHelper = pkg.default; 3 | import { join } from 'path'; 4 | 5 | const dir = process.cwd(); 6 | 7 | NpmPackageHelper.setHighestVersion([ 8 | NpmPackageHelper.getFileData(join(dir, 'package-lock.json')), 9 | NpmPackageHelper.getFileData(join(dir, 'package.json')), 10 | ]).then(console.log); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lenne.tech/cli", 3 | "version": "0.0.121", 4 | "description": "lenne.Tech CLI: lt", 5 | "keywords": [ 6 | "lenne.Tech", 7 | "cli", 8 | "lt" 9 | ], 10 | "author": "Kai Haase", 11 | "homepage": "http://lenne.tech", 12 | "license": "MIT", 13 | "repository": "https://github.com/lenneTech/cli.git", 14 | "bugs": { 15 | "url": "https://github.com/lenneTech/cli/issues" 16 | }, 17 | "bin": { 18 | "lt": "bin/lt" 19 | }, 20 | "scripts": { 21 | "build": "npm run lint && npm run test && npm run clean-build && npm run compile && npm run copy-templates", 22 | "clean-build": "npx rimraf ./build", 23 | "compile": "tsc -p .", 24 | "copy-templates": "npx shx cp -R ./src/templates ./build/templates", 25 | "coverage": "jest --coverage", 26 | "format": "prettier --write 'src/**/*.{js,ts,tsx,json}' '!src/templates/**/*'", 27 | "lint": "eslint './src/**/*.{ts,js,vue}'", 28 | "lint:fix": "eslint './src/**/*.{ts,js,vue}' --fix", 29 | "prepublishOnly": "npm run build", 30 | "preversion": "npm run lint", 31 | "reinit": "npx rimraf package-lock.json && npx rimraf node_modules && npm cache clean --force && npm i && npm run build", 32 | "snapupdate": "jest --updateSnapshot", 33 | "start": "node bin/lt", 34 | "start:build": "npm run build && node bin/lt --compiled-build", 35 | "start:compiled": "node bin/lt --compiled-build", 36 | "test": "jest --testTimeout=60000", 37 | "watch": "jest --watch", 38 | "release": "standard-version && git push --follow-tags origin main", 39 | "release:minor": "standard-version --release-as minor && git push --follow-tags origin main", 40 | "release:major": "standard-version --release-as major && git push --follow-tags origin main" 41 | }, 42 | "files": [ 43 | "tsconfig.json", 44 | "tslint.json", 45 | "build", 46 | "LICENSE", 47 | "README.md", 48 | "docs", 49 | "bin" 50 | ], 51 | "dependencies": { 52 | "@lenne.tech/cli-plugin-helper": "0.0.12", 53 | "bcrypt": "5.1.1", 54 | "find-file-up": "2.0.1", 55 | "glob": "11.0.2", 56 | "gluegun": "5.2.0", 57 | "js-sha256": "0.11.0", 58 | "open": "10.1.2", 59 | "standard-version": "9.5.0", 60 | "ts-morph": "25.0.1", 61 | "ts-node": "10.9.2", 62 | "typescript": "5.8.3" 63 | }, 64 | "devDependencies": { 65 | "@lenne.tech/eslint-config-ts": "2.0.1", 66 | "@lenne.tech/npm-package-helper": "0.0.12", 67 | "@types/jest": "29.5.14", 68 | "@types/node": "22.15.17", 69 | "@typescript-eslint/eslint-plugin": "8.32.0", 70 | "@typescript-eslint/parser": "8.32.0", 71 | "eslint": "9.26.0", 72 | "eslint-config-prettier": "10.1.5", 73 | "husky": "9.1.7", 74 | "jest": "29.7.0", 75 | "path-exists-cli": "2.0.0", 76 | "prettier": "3.5.3", 77 | "pretty-quick": "4.1.1", 78 | "rimraf": "6.0.1", 79 | "ts-jest": "29.3.2" 80 | }, 81 | "overrides": { 82 | "apisauce@*": "3.1.1", 83 | "cross-spawn@*": "7.0.6", 84 | "ejs@*": "3.1.10", 85 | "semver@*": "7.7.1" 86 | }, 87 | "jest": { 88 | "preset": "ts-jest", 89 | "testEnvironment": "node", 90 | "rootDir": "__tests__" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'gluegun'; 2 | import { join } from 'path'; 3 | 4 | /** 5 | * Create the cli and kick it off 6 | */ 7 | async function run(argv) { 8 | try { 9 | // Create a CLI runtime 10 | const cli = build() 11 | .brand('lt') 12 | .src(__dirname) 13 | // .plugins('./node_modules', { matching: 'lt-*', hidden: true }) 14 | .plugin(join(__dirname, '..', 'node_modules', '@lenne.tech', 'cli-plugin-helper', 'dist'), { 15 | commandFilePattern: ['*.js'], 16 | extensionFilePattern: ['*.js'], 17 | }) 18 | .help() // provides default for help, h, --help, -h 19 | .version() // provides default for version, v, --version, -v 20 | .create(); 21 | 22 | // Run cli 23 | const toolbox = await cli.run(argv); 24 | 25 | // Send it back (for testing, mostly) 26 | return toolbox; 27 | } catch (e) { 28 | // Abort via CTRL-C 29 | if (!e) { 30 | // eslint-disable-next-line no-console 31 | console.log('Goodbye ✌️'); 32 | } else { 33 | // Throw error 34 | throw e; 35 | } 36 | } 37 | } 38 | 39 | module.exports = { run }; 40 | -------------------------------------------------------------------------------- /src/commands/blocks/blocks.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Blocks commands 5 | */ 6 | module.exports = { 7 | alias: ['n'], 8 | description: 'Base blocks for Nuxt', 9 | hidden: true, 10 | name: 'blocks', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('blocks'); 13 | return 'blocks'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * CLI commands 5 | */ 6 | module.exports = { 7 | alias: ['c'], 8 | description: 'Commands to create a CLI', 9 | hidden: true, 10 | name: 'cli', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('cli', { headline: 'CLI commands' }); 13 | return 'cli'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/cli/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new CLI 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['c'], 10 | description: 'Creates a new CLI', 11 | hidden: false, 12 | name: 'create', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | git, 18 | helper, 19 | meta, 20 | parameters, 21 | print: { error, info, spin, success }, 22 | prompt: { ask }, 23 | strings: { kebabCase }, 24 | system, 25 | } = toolbox; 26 | 27 | // Info 28 | info('Create a new CLI'); 29 | 30 | // Check git 31 | if (!(await git.gitInstalled())) { 32 | return; 33 | } 34 | 35 | // Get name 36 | const name = await helper.getInput(parameters.first, { 37 | name: 'CLI name', 38 | showError: true, 39 | }); 40 | if (!name) { 41 | return; 42 | } 43 | 44 | // Get author 45 | const author = await helper.getInput(parameters.options.author, { 46 | name: 'Author', 47 | showError: true, 48 | }); 49 | 50 | // Link 51 | let link = parameters.options.link && !parameters.options.nolink; 52 | if (!parameters.options.link && !parameters.options.nolink) { 53 | link = !!(await ask({ 54 | message: 'Link when finished?', 55 | name: 'link', 56 | type: 'confirm', 57 | })).link; 58 | } 59 | 60 | // Start timer 61 | const timer = system.startTimer(); 62 | 63 | // Set project directory 64 | const projectDir = kebabCase(name); // kebab-case 65 | 66 | // Check if directory already exists 67 | if (filesystem.exists(projectDir)) { 68 | info(''); 69 | error(`There's already a folder named "${projectDir}" here.`); 70 | return undefined; 71 | } 72 | 73 | // Clone git repository 74 | const cloneSpinner = spin('Clone https://github.com/lenneTech/cli-starter.git'); 75 | await system.run(`git clone https://github.com/lenneTech/cli-starter.git ${projectDir}`); 76 | if (filesystem.isDirectory(`./${projectDir}`)) { 77 | filesystem.remove(`./${projectDir}/.git`); 78 | cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/cli-starter.git'); 79 | } else { 80 | cloneSpinner.fail(`The directory "${projectDir}" could not be created.`); 81 | return undefined; 82 | } 83 | 84 | // Install packages 85 | const installSpinner = spin('Install npm packages'); 86 | await system.run(`cd ${projectDir} && npm i`); 87 | installSpinner.succeed('NPM packages installed'); 88 | 89 | // Rename files and data 90 | const renameSpinner = spin(`Rename files & data ${link ? ' and link' : ''}`); 91 | await system.run( 92 | `cd ${projectDir} && npm run rename -- "${name}" --author "${author}" --${link ? 'link' : 'nolink'}`, 93 | ); 94 | renameSpinner.succeed(`Files & data renamed${link ? ' and linked' : ''}`); 95 | 96 | // Init git 97 | const initGitSpinner = spin('Initialize git'); 98 | await system.run( 99 | `cd ${projectDir} && git init && git add . && git commit -am "Init via lenne.Tech CLI ${meta.version()}"`, 100 | ); 101 | initGitSpinner.succeed('Git initialized'); 102 | 103 | // We're done, so show what to do next 104 | info(''); 105 | success( 106 | `Generated ${name} server with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`, 107 | ); 108 | 109 | // For tests 110 | return `new cli ${name}`; 111 | }, 112 | }; 113 | 114 | export default NewCommand; 115 | -------------------------------------------------------------------------------- /src/commands/cli/rename.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { dirname } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Rename current CLI 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['r'], 11 | description: 'Rename current CLI', 12 | hidden: false, 13 | name: 'rename', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | npm, 18 | parameters, 19 | print: { error }, 20 | system, 21 | } = toolbox; 22 | 23 | // Get root path 24 | const { path: packagePath } = await npm.getPackageJson(); 25 | if (!packagePath) { 26 | error('The path to the root directory could not be found.'); 27 | return undefined; 28 | } 29 | const rootPath = dirname(packagePath); 30 | if (!rootPath) { 31 | error('The path to the root directory could not be found.'); 32 | return undefined; 33 | } 34 | 35 | // Run rename script 36 | await system.run(`cd ${rootPath} && npm run rename -- ${parameters.string}`); 37 | 38 | // For tests 39 | return 'Rename current CLI'; 40 | }, 41 | }; 42 | 43 | export default NewCommand; 44 | -------------------------------------------------------------------------------- /src/commands/components/components.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Components commands 5 | */ 6 | module.exports = { 7 | alias: ['n'], 8 | description: 'Base components for Nuxt', 9 | hidden: true, 10 | name: 'components', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('components'); 13 | return 'components'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/deployment/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { join } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Create a new server module 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['dc'], 11 | description: 'Creates a new deployment for mono repository', 12 | hidden: false, 13 | name: 'create', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | filesystem, 18 | helper, 19 | parameters, 20 | patching, 21 | print: { info, spin, success }, 22 | prompt: { confirm }, 23 | strings: { camelCase, kebabCase, pascalCase }, 24 | system, 25 | template, 26 | } = toolbox; 27 | 28 | // Start timer 29 | const timer = system.startTimer(); 30 | 31 | // Info 32 | info('Create a new deployment'); 33 | 34 | // Get default project name 35 | let projectName = ''; 36 | const config = await filesystem.exists('lt.json'); 37 | if (config) { 38 | await patching.update('lt.json', (data: Record) => { 39 | projectName = data.name; 40 | return data; 41 | }); 42 | } 43 | 44 | if (!projectName) { 45 | await patching.update('package.json', (data: Record) => { 46 | projectName = pascalCase(data.name); 47 | return data; 48 | }); 49 | } 50 | 51 | // Get name 52 | const name = await helper.getInput(parameters.first, { 53 | initial: projectName, 54 | name: `project name (e.g. ${projectName ? projectName : 'My new project'})`, 55 | }); 56 | 57 | if (!name) { 58 | return; 59 | } 60 | 61 | // Get domain 62 | const domain = await helper.getInput(parameters.second, { 63 | initial: `${kebabCase(name)}.lenne.tech`, 64 | name: `main domain of the project (e.g. ${kebabCase(name)}.lenne.tech)`, 65 | }); 66 | 67 | if (!name) { 68 | return; 69 | } 70 | 71 | const gitHub = await confirm('Add GitHub pipeline?'); 72 | const gitLab = await confirm('Add GitLab pipeline?'); 73 | 74 | // GitLab test runner 75 | let testRunner; 76 | let prodRunner; 77 | if (gitLab) { 78 | testRunner = await helper.getInput('', { 79 | initial: 'docker-swarm', 80 | name: 'runner for test (tag in .gitlab-ci.yml, e.g. docker-swarm)', 81 | }); 82 | if (!testRunner) { 83 | return; 84 | } 85 | prodRunner = await helper.getInput('', { 86 | initial: 'docker-landing', 87 | name: 'runner for production (tag in .gitlab-ci.yml, e.g. docker-landing)', 88 | }); 89 | if (!prodRunner) { 90 | return; 91 | } 92 | } 93 | 94 | // Set up initial props (to pass into templates) 95 | const nameCamel = camelCase(name); 96 | const nameKebab = kebabCase(name); 97 | const namePascal = pascalCase(name); 98 | 99 | // Check if directory 100 | const cwd = filesystem.cwd(); 101 | 102 | const generateSpinner = spin('Generate files'); 103 | 104 | await template.generate({ 105 | props: { nameCamel, nameKebab, namePascal }, 106 | target: join(cwd, 'scripts', 'build-push.sh'), 107 | template: 'deployment/scripts/build-push.sh.ejs', 108 | }); 109 | 110 | await template.generate({ 111 | props: { nameCamel, nameKebab, namePascal }, 112 | target: join(cwd, 'scripts', 'deploy.sh'), 113 | template: 'deployment/scripts/deploy.sh.ejs', 114 | }); 115 | 116 | await template.generate({ 117 | props: { nameCamel, nameKebab, namePascal }, 118 | target: join(cwd, 'Dockerfile'), 119 | template: 'deployment/Dockerfile.ejs', 120 | }); 121 | 122 | await template.generate({ 123 | props: { nameCamel, nameKebab, namePascal }, 124 | target: join(cwd, 'Dockerfile.app'), 125 | template: 'deployment/Dockerfile.app.ejs', 126 | }); 127 | 128 | await template.generate({ 129 | props: { nameCamel, nameKebab, namePascal }, 130 | target: join(cwd, 'docker-compose.dev.yml'), 131 | template: 'deployment/docker-compose.dev.yml.ejs', 132 | }); 133 | 134 | await template.generate({ 135 | props: { nameCamel, nameKebab, namePascal }, 136 | target: join(cwd, 'docker-compose.test.yml'), 137 | template: 'deployment/docker-compose.test.yml.ejs', 138 | }); 139 | 140 | await template.generate({ 141 | props: { nameCamel, nameKebab, namePascal }, 142 | target: join(cwd, 'docker-compose.prod.yml'), 143 | template: 'deployment/docker-compose.prod.yml.ejs', 144 | }); 145 | 146 | if (gitHub) { 147 | await template.generate({ 148 | props: { nameCamel, nameKebab, namePascal, url: domain }, 149 | target: join(cwd, '.github', 'workflows', 'pre-release.yml'), 150 | template: 'deployment/.github/workflows/pre-release.yml.ejs', 151 | }); 152 | 153 | await template.generate({ 154 | props: { nameCamel, nameKebab, namePascal, url: domain }, 155 | target: join(cwd, '.github', 'workflows', 'release.yml'), 156 | template: 'deployment/.github/workflows/release.yml.ejs', 157 | }); 158 | } 159 | 160 | if (gitLab) { 161 | await template.generate({ 162 | props: { nameCamel, nameKebab, namePascal, prodRunner, testRunner, url: domain }, 163 | target: join(cwd, '.gitlab-ci.yml'), 164 | template: 'deployment/.gitlab-ci.yml.ejs', 165 | }); 166 | } 167 | 168 | generateSpinner.succeed('Files generated'); 169 | 170 | const environmentsSpinner = spin('Update app environment files'); 171 | const prodEnv = await filesystem.exists('projects/app/src/environments/environment.prod.ts'); 172 | if (prodEnv) { 173 | await patching.patch('projects/app/src/environments/environment.prod.ts', { 174 | insert: `https://api.${domain}`, 175 | replace: new RegExp('http://127.0.0.1:3000', 'g'), 176 | }); 177 | await patching.patch('projects/app/src/environments/environment.prod.ts', { 178 | insert: `wss://api.${domain}`, 179 | replace: new RegExp('ws://127.0.0.1:3000', 'g'), 180 | }); 181 | await patching.patch('projects/app/src/environments/environment.prod.ts', { 182 | insert: `https://${domain}`, 183 | replace: new RegExp('http://127.0.0.1:4200', 'g'), 184 | }); 185 | } else { 186 | info('Missing projects/app/src/environments/environment.prod.ts'); 187 | } 188 | 189 | const testEnv = await filesystem.exists('projects/app/src/environments/environment.test.ts'); 190 | if (testEnv) { 191 | await patching.patch('projects/app/src/environments/environment.test.ts', { 192 | insert: `https://api.test.${domain}`, 193 | replace: new RegExp('http://127.0.0.1:3000', 'g'), 194 | }); 195 | await patching.patch('projects/app/src/environments/environment.test.ts', { 196 | insert: `wss://api.test.${domain}`, 197 | replace: new RegExp('ws://127.0.0.1:3000', 'g'), 198 | }); 199 | await patching.patch('projects/app/src/environments/environment.test.ts', { 200 | insert: `https://test.${domain}`, 201 | replace: new RegExp('http://127.0.0.1:4200', 'g'), 202 | }); 203 | } else { 204 | info('Missing projects/app/src/environments/environment.test.ts'); 205 | } 206 | 207 | environmentsSpinner.succeed('App environment files updated'); 208 | 209 | // We're done, so show what to do next 210 | info(''); 211 | success(`Generated deployment for ${namePascal} in ${helper.msToMinutesAndSeconds(timer())}m.`); 212 | info(''); 213 | 214 | // Hint for CI/CD 215 | const subDomains = ['www', 'api', 'test', 'www.test', 'api.test']; 216 | let urlStr = `\n- ${domain}`; 217 | for (const sub of subDomains) { 218 | urlStr += `\n- ${sub}.${domain}`; 219 | } 220 | success(`HINT: please initialize following Domains before running the CI/CD pipeline:${urlStr}`); 221 | info(''); 222 | 223 | if (!toolbox.parameters.options.fromGluegunMenu) { 224 | process.exit(); 225 | } 226 | 227 | // For tests 228 | return `new deployment ${name}`; 229 | }, 230 | }; 231 | 232 | export default NewCommand; 233 | -------------------------------------------------------------------------------- /src/commands/deployment/deployment.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Deployment commands 5 | */ 6 | module.exports = { 7 | alias: ['d'], 8 | description: 'Server commands', 9 | hidden: true, 10 | name: 'deployment', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('deployment'); 13 | return 'deployment'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/docs/docs.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Docs commands 5 | */ 6 | module.exports = { 7 | alias: ['d'], 8 | description: 'Docs commands', 9 | hidden: true, 10 | name: 'docs', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('docs'); 13 | return 'docs'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/docs/open.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Open documentations 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['o'], 10 | description: 'Open documentation', 11 | hidden: false, 12 | name: 'open', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | helper, 17 | parameters, 18 | print: { error }, 19 | prompt: { ask }, 20 | } = toolbox; 21 | 22 | const { default: open } = await import('open'); 23 | 24 | const choices = ['lenne.Tech', 'NestJS', 'GlueGun']; 25 | 26 | // Get input 27 | let input = await helper.getInput(parameters.first, { 28 | name: 'doc', 29 | showError: true, 30 | }); 31 | if (!input || !choices.includes(input)) { 32 | // Select type 33 | const { type } = await ask({ 34 | choices: choices.slice(0), 35 | message: 'Select', 36 | name: 'type', 37 | type: 'select', 38 | }); 39 | input = type; 40 | } 41 | 42 | switch (input) { 43 | case choices[0]: { 44 | await open('http://lenne.tech'); 45 | break; 46 | } 47 | case choices[1]: { 48 | await open('https://docs.nestjs.com/'); 49 | break; 50 | } 51 | case choices[2]: { 52 | await open('https://infinitered.github.io/gluegun/#/?id=quick-start'); 53 | break; 54 | } 55 | default: { 56 | error(`${input} not found!`); 57 | return; 58 | } 59 | } 60 | 61 | // For tests 62 | return 'docs open'; 63 | }, 64 | }; 65 | 66 | export default NewCommand; 67 | -------------------------------------------------------------------------------- /src/commands/frontend/angular.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new Angular workspace 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['a'], 10 | description: 'Creates a new Angular workspace', 11 | hidden: false, 12 | name: 'angular', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | git, 18 | helper, 19 | parameters, 20 | patching, 21 | print: { error, info, spin, success }, 22 | prompt: { confirm }, 23 | strings: { kebabCase }, 24 | system, 25 | } = toolbox; 26 | 27 | // Start timer 28 | const timer = system.startTimer(); 29 | 30 | // Info 31 | info('Create a new Angular workspace'); 32 | 33 | // Check git 34 | if (!(await git.gitInstalled())) { 35 | return; 36 | } 37 | 38 | // Get name of the workspace 39 | const name = await helper.getInput(parameters.first, { 40 | name: 'workspace name', 41 | showError: true, 42 | }); 43 | if (!name) { 44 | return; 45 | } 46 | 47 | // Set project directory 48 | const projectDir = kebabCase(name); 49 | 50 | // Check if directory already exists 51 | if (filesystem.exists(projectDir)) { 52 | info(''); 53 | error(`There's already a folder named "${projectDir}" here.`); 54 | return undefined; 55 | } 56 | 57 | // Localize 58 | const localize 59 | = parameters.second?.toLowerCase().includes('localize') 60 | || (!parameters.second && (await confirm('Init localize for Angular?', true))); 61 | 62 | const gitLink = ( 63 | await helper.getInput(null, { 64 | name: 'Provide the URL of an empty repository (e.g., git@example.com:group/project.git, or leave empty to skip linking)', 65 | showError: false, 66 | }) 67 | ).trim(); 68 | 69 | const workspaceSpinner = spin(`Creating angular workspace ${projectDir}...`); 70 | 71 | // Clone monorepo 72 | await system.run(`git clone https://github.com/lenneTech/ng-base-starter ${projectDir}`); 73 | 74 | // Check for directory 75 | if (!filesystem.isDirectory(`./${projectDir}`)) { 76 | error(`The directory '${projectDir}' was not created.`); 77 | return undefined; 78 | } 79 | 80 | // Remove git folder after clone 81 | filesystem.remove(`${projectDir}/.git`); 82 | 83 | 84 | // Install packages 85 | await system.run(`cd ${projectDir} && npm i`); 86 | 87 | // Check if git init is active 88 | 89 | const gitSpinner = spin('Initializing git...'); 90 | await system.run(`cd ${projectDir} && git init --initial-branch=main`); 91 | gitSpinner.succeed('Successfully initialized Git'); 92 | if (gitLink) { 93 | await system.run(`cd ${projectDir} && git remote add origin ${gitLink}`); 94 | await system.run(`cd ${projectDir} && git add .`); 95 | await system.run(`cd ${projectDir} && git commit -m "Initial commit"`); 96 | await system.run(`cd ${projectDir} && git push -u origin main`); 97 | } 98 | 99 | workspaceSpinner.succeed(`Workspace ${projectDir} created`); 100 | 101 | if (filesystem.isDirectory(`./${projectDir}`)) { 102 | 103 | // Remove husky from app project 104 | filesystem.remove(`${projectDir}/.husky`); 105 | await patching.update(`${projectDir}/package.json`, (data: Record) => { 106 | delete data.scripts.prepare; 107 | delete data.devDependencies.husky; 108 | return data; 109 | }); 110 | 111 | if (localize) { 112 | const localizeSpinner = spin('Adding localization for Angular...'); 113 | await system.run(`cd ${projectDir} && ng add @angular/localize --skip-confirmation`); 114 | localizeSpinner.succeed('Added localization for Angular'); 115 | } 116 | 117 | // Install all packages 118 | const installSpinner = spin('Install all packages'); 119 | await system.run(`cd ${projectDir} && npm run init`); 120 | installSpinner.succeed('Successfully installed all packages'); 121 | 122 | // We're done, so show what to do next 123 | info(''); 124 | success(`Generated Angular workspace ${projectDir} in ${helper.msToMinutesAndSeconds(timer())}m.`); 125 | info(''); 126 | info('Next:'); 127 | info(` Test and run ${name}:`); 128 | info(` $ cd ${projectDir}`); 129 | info(' $ npm run test'); 130 | info(' $ npm run start'); 131 | info(''); 132 | 133 | if (!toolbox.parameters.options.fromGluegunMenu) { 134 | process.exit(); 135 | } 136 | 137 | // For tests 138 | return `new workspace ${projectDir} with ${name}`; 139 | } 140 | }, 141 | }; 142 | 143 | export default NewCommand; 144 | -------------------------------------------------------------------------------- /src/commands/frontend/frontend.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Frontend commands 5 | */ 6 | module.exports = { 7 | alias: ['f'], 8 | description: 'Frontend commands', 9 | hidden: true, 10 | name: 'frontend', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('frontend'); 13 | return 'frontend'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/frontend/nuxt.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new nuxt workspace 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['n'], 10 | description: 'Creates a new nuxt workspace', 11 | hidden: false, 12 | name: 'nuxt', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | helper, 17 | print: { spin }, 18 | prompt: { ask }, 19 | strings: { kebabCase }, 20 | system, 21 | } = toolbox; 22 | 23 | 24 | const projName = ( 25 | await ask({ 26 | message: 'What is the project\'s name?', 27 | name: 'projectName', 28 | required: true, 29 | type: 'input', 30 | }) 31 | ).projectName; 32 | 33 | // Start timer 34 | const timer = system.startTimer(); 35 | 36 | const baseSpinner = spin(`Creating nuxt-base with name '${kebabCase(projName)}'`); 37 | 38 | await system.run(`npx create-nuxt-base '${kebabCase(projName)}'`); 39 | 40 | 41 | baseSpinner.succeed(`Successfully created nuxt workspace with name '${kebabCase(projName)}' in ${helper.msToMinutesAndSeconds( 42 | timer(), 43 | )}m.`); 44 | 45 | if (!toolbox.parameters.options.fromGluegunMenu) { 46 | process.exit(); 47 | } 48 | 49 | }, 50 | }; 51 | 52 | export default NewCommand; 53 | -------------------------------------------------------------------------------- /src/commands/fullstack/fullstack.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Fullstack commands 5 | */ 6 | module.exports = { 7 | alias: ['full'], 8 | description: 'Fullstack commands', 9 | hidden: true, 10 | name: 'fullstack', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('fullstack'); 13 | return 'fullstack'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/fullstack/init.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand, patching } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | 6 | /** 7 | * Create a new server 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['init'], 11 | description: 'Creates a new fullstack workspace', 12 | hidden: false, 13 | name: 'init', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | filesystem, 18 | git, 19 | helper, 20 | parameters, 21 | print: { error, info, spin, success }, 22 | prompt: { ask, confirm }, 23 | server, 24 | strings: { kebabCase }, 25 | system, 26 | } = toolbox; 27 | 28 | // Start timer 29 | const timer = system.startTimer(); 30 | 31 | // Info 32 | info('Create a new fullstack workspace'); 33 | 34 | // Check git 35 | if (!(await git.gitInstalled())) { 36 | return; 37 | } 38 | 39 | // Get name of the workspace 40 | const name = await helper.getInput(parameters.first, { 41 | name: 'workspace name', 42 | showError: true, 43 | }); 44 | if (!name) { 45 | return; 46 | } 47 | 48 | // Set project directory 49 | const projectDir = kebabCase(name); 50 | 51 | // Check if directory already exists 52 | if (filesystem.exists(projectDir)) { 53 | info(''); 54 | error(`There's already a folder named "${projectDir}" here.`); 55 | return undefined; 56 | } 57 | 58 | let frontend = ( 59 | await ask({ 60 | message: 'Angular (a) or Nuxt 3 (n)', 61 | name: 'frontend', 62 | type: 'input', 63 | }) 64 | ).frontend; 65 | 66 | if (frontend === 'a') { 67 | frontend = 'angular'; 68 | } else if (frontend === 'n') { 69 | frontend = 'nuxt'; 70 | } else { 71 | process.exit(); 72 | } 73 | 74 | let addToGit = false; 75 | let gitLink; 76 | if (parameters.third !== 'false') { 77 | addToGit = parameters.third === 'true' || (await confirm('Add workspace to a new git repository?')); 78 | 79 | // Check if git init is active 80 | if (addToGit) { 81 | // Get name of the app 82 | gitLink = await helper.getInput(null, { 83 | name: 'git repository link', 84 | showError: true, 85 | }); 86 | if (!gitLink) { 87 | addToGit = false; 88 | } 89 | } 90 | } 91 | 92 | const workspaceSpinner = spin(`Create fullstack workspace with ${frontend} in ${projectDir} with ${name} app`); 93 | 94 | // Clone monorepo 95 | await system.run(`git clone https://github.com/lenneTech/lt-monorepo.git ${projectDir}`); 96 | 97 | // Check for directory 98 | if (!filesystem.isDirectory(`./${projectDir}`)) { 99 | error(`The directory "${projectDir}" could not be created.`); 100 | return undefined; 101 | } 102 | 103 | workspaceSpinner.succeed(`Create fullstack workspace with ${frontend} in ${projectDir} for ${name} created`); 104 | 105 | // Include example app 106 | const ngBaseSpinner = spin(`Integrate example for ${frontend}`); 107 | 108 | // Remove git folder after clone 109 | filesystem.remove(`${projectDir}/.git`); 110 | 111 | // Check if git init is active 112 | if (addToGit) { 113 | await system.run(`cd ${projectDir} && git init --initial-branch=dev`); 114 | await system.run(`cd ${projectDir} && git remote add origin ${gitLink}`); 115 | await system.run(`cd ${projectDir} && git add .`); 116 | await system.run(`cd ${projectDir} && git commit -m "Initial commit"`); 117 | await system.run(`cd ${projectDir} && git push -u origin dev`); 118 | } 119 | 120 | if (frontend === 'angular') { 121 | // Clone ng-base-starter 122 | await system.run(`cd ${projectDir}/projects && git clone https://github.com/lenneTech/ng-base-starter.git app`); 123 | } else { 124 | await system.run('npm i -g create-nuxt-base'); 125 | await system.run(`cd ${projectDir}/projects && create-nuxt-base app`); 126 | } 127 | 128 | // Remove gitkeep file 129 | filesystem.remove(`${projectDir}/projects/.gitkeep`); 130 | 131 | // Remove git folder after clone 132 | filesystem.remove(`${projectDir}/projects/app/.git`); 133 | 134 | // Integrate files 135 | if (filesystem.isDirectory(`./${projectDir}/projects/app`)) { 136 | // Check if git init is active 137 | if (addToGit) { 138 | // Commit changes 139 | await system.run( 140 | `cd ${projectDir} && git add . && git commit -am "feat: ${frontend} example integrated" && git push`, 141 | ); 142 | } 143 | 144 | // Angular example integration done 145 | ngBaseSpinner.succeed(`Example for ${frontend} integrated`); 146 | 147 | // Include files from https://github.com/lenneTech/nest-server-starter 148 | 149 | // Init 150 | const serverSpinner = spin('Integrate Nest Server Starter'); 151 | 152 | // Clone api 153 | await system.run(`cd ${projectDir}/projects && git clone https://github.com/lenneTech/nest-server-starter api`); 154 | 155 | // Integrate files 156 | if (filesystem.isDirectory(`./${projectDir}/projects/api`)) { 157 | // Remove git folder from clone 158 | filesystem.remove(`${projectDir}/projects/api/.git`); 159 | 160 | // Prepare meta.json in api 161 | filesystem.write(`./${projectDir}/projects/api/src/meta.json`, { 162 | description: `API for ${name} app`, 163 | name: `${name}-api-server`, 164 | version: '0.0.0', 165 | }); 166 | 167 | // Replace secret or private keys and remove `nest-server` 168 | await patching.update(`./${projectDir}/projects/api/src/config.env.ts`, content => server.replaceSecretOrPrivateKeys(content).replace(/nest-server-/g, `${projectDir 169 | }-`)); 170 | 171 | // Check if git init is active 172 | if (addToGit) { 173 | // Commit changes 174 | await system.run( 175 | `cd ${projectDir} && git add . && git commit -am "feat: Nest Server Starter integrated" && git push`, 176 | ); 177 | } 178 | 179 | // Done 180 | serverSpinner.succeed('Nest Server Starter integrated'); 181 | } else { 182 | serverSpinner.warn('Nest Server Starter not integrated'); 183 | } 184 | 185 | // Install all packages 186 | const installSpinner = spin('Install all packages'); 187 | await system.run(`cd ${projectDir} && npm i && npm run init`); 188 | installSpinner.succeed('Successfull installed all packages'); 189 | 190 | // We're done, so show what to do next 191 | info(''); 192 | success( 193 | `Generated fullstack workspace with ${frontend} in ${projectDir} with ${name} app in ${helper.msToMinutesAndSeconds( 194 | timer(), 195 | )}m.`, 196 | ); 197 | info(''); 198 | info('Next:'); 199 | info(` Run ${name}`); 200 | info(` $ cd ${projectDir}`); 201 | info(' $ npm run start'); 202 | info(''); 203 | 204 | if (!toolbox.parameters.options.fromGluegunMenu) { 205 | process.exit(); 206 | } 207 | 208 | // For tests 209 | return `new workspace ${projectDir} with ${name}`; 210 | } 211 | }, 212 | }; 213 | 214 | export default NewCommand; 215 | -------------------------------------------------------------------------------- /src/commands/git/clean.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand, system } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Removed local merged branches 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['rm'], 10 | description: 'Removed local merged branches', 11 | hidden: false, 12 | name: 'clean', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | print: { info, spin, success }, 19 | system: { run, startTimer }, 20 | } = toolbox; 21 | 22 | // Check git 23 | if (!(await git.gitInstalled())) { 24 | return; 25 | } 26 | 27 | // Get current branch 28 | const currentBranch = await git.currentBranch(); 29 | 30 | let branch; 31 | if (currentBranch !== 'dev' || currentBranch !== 'main') { 32 | // Search for branch, which includes branch name 33 | branch = await git.getBranch('dev', { 34 | error: false, 35 | exact: false, 36 | remote: false, 37 | spin: true, 38 | }); 39 | 40 | if (branch !== 'dev') { 41 | branch = await git.getBranch('main', { 42 | error: true, 43 | exact: false, 44 | remote: false, 45 | spin: true, 46 | }); 47 | } 48 | 49 | await run(`git checkout ${branch}`); 50 | info(`Changed Branch to ${branch}`); 51 | } 52 | 53 | // Start timer 54 | const timer = startTimer(); 55 | 56 | // Reset soft 57 | const undoSpinner = spin('Start cleaning\n'); 58 | 59 | const resultFetch = await run('git fetch -p'); 60 | info(resultFetch); 61 | 62 | const resultpull = await run('git pull'); 63 | info(resultpull); 64 | 65 | const result = await system.run('git branch --merged'); 66 | const excludedBranches = ['main', 'dev', 'develop', 'beta', 'intern', 'release']; 67 | 68 | // Local Branches into Array 69 | const branches = result 70 | .split('\n') 71 | .map(branch => branch.trim().replace(/^\* /, '')) // Remove '* ' 72 | .filter(branch => branch && !excludedBranches.includes(branch)); 73 | 74 | if (branches.length === 0) { 75 | info('No branches to delete.'); 76 | return; 77 | } 78 | 79 | info(`Deleting branches: ${branches.join(', ')}`); 80 | 81 | // Delete branches 82 | for (const branch of branches) { 83 | await system.run(`git branch -d ${branch}`); 84 | success(`Deleted branch: ${branch}`); 85 | } 86 | 87 | undoSpinner.succeed(); 88 | 89 | // Success 90 | success(`Successfull cleaned in ${helper.msToMinutesAndSeconds(timer())}m.`); 91 | info(''); 92 | 93 | // For tests 94 | return 'cleaned local'; 95 | }, 96 | }; 97 | 98 | export default NewCommand; 99 | -------------------------------------------------------------------------------- /src/commands/git/clear.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Undo current changes 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['cl'], 10 | description: 'Undo current changes', 11 | hidden: false, 12 | name: 'clear', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { info, spin, success }, 20 | prompt: { confirm }, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Get current branch 30 | const branch = await git.currentBranch(); 31 | 32 | // Ask to squash the branch 33 | if (!parameters.options.noConfirm && !(await confirm(`Clear "${branch}"?`))) { 34 | return; 35 | } 36 | 37 | // Start timer 38 | const timer = startTimer(); 39 | 40 | // Reset soft 41 | const undoSpinner = spin(`Clear ${branch}`); 42 | await run('git reset --hard && git clean -fd'); 43 | undoSpinner.succeed(); 44 | 45 | // Success 46 | success(`Clear ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 47 | info(''); 48 | 49 | // For tests 50 | return `clear branch ${branch}`; 51 | }, 52 | }; 53 | 54 | export default NewCommand; 55 | -------------------------------------------------------------------------------- /src/commands/git/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['c'], 10 | description: 'Create a new branch', 11 | hidden: false, 12 | name: 'create', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | npm, 19 | parameters, 20 | print: { error, info, spin, success }, 21 | system, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Check changes in current branch (reset optional) 30 | await git.askForReset(); 31 | 32 | // Get branch 33 | const branch = await helper.getInput(parameters.first, { 34 | name: 'branch name', 35 | showError: true, 36 | }); 37 | if (!branch) { 38 | return; 39 | } 40 | 41 | // Check if branch already exists 42 | if (await git.getBranch(branch)) { 43 | error(`Branch ${branch} already exists!`); 44 | } 45 | 46 | // Select base branch 47 | let baseBranch = parameters.second; 48 | if (!baseBranch || !(await git.getBranch(baseBranch))) { 49 | baseBranch = await git.selectBranch({ text: 'Select base branch' }); 50 | } 51 | 52 | // Start timer 53 | const timer = system.startTimer(); 54 | 55 | // Checkout 56 | const createSpin = spin(`Create ${branch}`); 57 | await system.run('git fetch &&' + `git checkout ${baseBranch} &&` + 'git pull && ' + `git checkout -b ${branch}`); 58 | createSpin.succeed(); 59 | 60 | // Install npm packages 61 | await npm.install(); 62 | 63 | // Success info 64 | success(`Branch ${branch} was created in ${helper.msToMinutesAndSeconds(timer())}m.`); 65 | info(''); 66 | 67 | // For tests 68 | return `created branch ${branch}`; 69 | }, 70 | }; 71 | 72 | export default NewCommand; 73 | -------------------------------------------------------------------------------- /src/commands/git/force-pull.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Pull branch with loosing changes 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['pf', 'pull-force'], 10 | description: 'Pull branch with loosing changes', 11 | hidden: false, 12 | name: 'force-pull', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { info, spin, success }, 20 | prompt, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Get current branch 30 | const branch = await git.currentBranch(); 31 | 32 | // Ask for reset 33 | if (!parameters.options.noConfirm && !(await prompt.confirm('You will lose your changes, are you sure?'))) { 34 | return; 35 | } 36 | 37 | // Start timer 38 | const timer = startTimer(); 39 | 40 | // Reset soft 41 | const pullSpinner = spin(`Fetch and pull ${branch}`); 42 | await run(`git fetch && git reset origin/${branch} --hard`); 43 | pullSpinner.succeed(); 44 | 45 | // Success 46 | success(`Pull ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 47 | info(''); 48 | 49 | // For tests 50 | return `pull branch ${branch}`; 51 | }, 52 | }; 53 | 54 | export default NewCommand; 55 | -------------------------------------------------------------------------------- /src/commands/git/get.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Checkout git branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['g'], 10 | description: 'Checkout git branch', 11 | hidden: false, 12 | name: 'get', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | git, 18 | helper, 19 | npm, 20 | parameters, 21 | print: { error, info, spin, success }, 22 | prompt, 23 | system, 24 | } = toolbox; 25 | 26 | // Start timer 27 | const timer = system.startTimer(); 28 | 29 | // Check git 30 | if (!(await git.gitInstalled())) { 31 | return; 32 | } 33 | 34 | // Get (part of) branch name 35 | const branchName = await helper.getInput(parameters.first, { 36 | name: 'branch name', 37 | showError: true, 38 | }); 39 | if (!branchName) { 40 | return; 41 | } 42 | 43 | // Check changes in current branch (reset necessary) 44 | if (!(await git.askForReset({ showError: true }))) { 45 | return; 46 | } 47 | 48 | // Search for branch, which includes branch name 49 | const branch = await git.getBranch(branchName, { 50 | error: true, 51 | exact: false, 52 | remote: false, 53 | spin: true, 54 | }); 55 | info(`Found branch ${branch} for ${branchName}`); 56 | if (!branch) { 57 | return; 58 | } 59 | 60 | // Get remote branch 61 | const remoteBranch = await git.getBranch(branch, { remote: true }); 62 | 63 | // Ask for checkout branch 64 | if (branchName !== branch) { 65 | if ( 66 | !parameters.options.noConfirm 67 | && !(await prompt.confirm(`Checkout ${remoteBranch ? 'remote' : 'local'} branch ${branch}`)) 68 | ) { 69 | return; 70 | } 71 | } 72 | 73 | // Checkout branch 74 | await system.run(`git checkout ${(await git.getDefaultBranch()) || 'main'}`); 75 | let checkoutSpin; 76 | 77 | // Handling for remote 78 | if (remoteBranch) { 79 | // Delete local 80 | let removed = false; 81 | const checkSpin = spin('Check status'); 82 | if (branch !== 'main' && branch && (await git.diffFiles(branch, { noDiffResult: '' })).length) { 83 | checkSpin.succeed(); 84 | let mode = parameters.options.mode; 85 | if (!mode) { 86 | if (await prompt.confirm(`Remove local commits of ${branch}`)) { 87 | mode = 'hard'; 88 | } 89 | } 90 | if (mode === 'hard') { 91 | const prepareSpin = spin(`Refresh ${branch}`); 92 | await system.run(`git branch -D ${branch}`); 93 | removed = true; 94 | prepareSpin.succeed(); 95 | } 96 | } else { 97 | checkSpin.succeed(); 98 | } 99 | 100 | // Start spin 101 | checkoutSpin = spin(`Checkout ${branch}`); 102 | 103 | // Checkout remote if local branch not exists 104 | if (removed || !(await git.getBranch(branch, { local: true }))) { 105 | await system.run( 106 | `git fetch && git checkout --track origin/${branch} && git reset --hard && git clean -fd && git pull`, 107 | ); 108 | 109 | // Checkout local branch 110 | } else { 111 | await system.run(`git fetch && git checkout ${branch} && git reset --hard && git clean -fd && git pull`); 112 | } 113 | 114 | // Handling for local only 115 | } else if (branch) { 116 | checkoutSpin = spin(`Checkout ${branch}`); 117 | await system.run(`git fetch && git checkout ${branch} && git reset --hard && git clean -fd`); 118 | 119 | // No branch found 120 | } else { 121 | error(`Branch ${branch} not found!`); 122 | return; 123 | } 124 | 125 | // Checkout done 126 | checkoutSpin.succeed(); 127 | 128 | // Install npm packages 129 | await npm.install(); 130 | 131 | // Init lerna projects 132 | if (filesystem.isFile('./lerna.json')) { 133 | const initProjectsSpin = spin('Init projects'); 134 | await system.run('npm run init --if-present'); 135 | initProjectsSpin.succeed(); 136 | } 137 | 138 | // Success info 139 | success( 140 | `${remoteBranch ? 'Remote' : 'Local'} branch ${branch} checked out in ${helper.msToMinutesAndSeconds(timer())}m.`, 141 | ); 142 | info(''); 143 | if (!toolbox.parameters.options.fromGluegunMenu) { 144 | process.exit(); 145 | } 146 | 147 | // For tests 148 | return `get branch ${branch}`; 149 | }, 150 | }; 151 | 152 | export default NewCommand; 153 | -------------------------------------------------------------------------------- /src/commands/git/git.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Git commands 5 | */ 6 | module.exports = { 7 | alias: ['g'], 8 | description: 'Git commands', 9 | hidden: true, 10 | name: 'git', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('git'); 13 | return 'git'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/git/rebase.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Rebase branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['rb'], 10 | description: 'Rebase branch', 11 | hidden: false, 12 | name: 'rebase', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { error, info, spin, success }, 20 | prompt: { confirm }, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Get current branch 30 | const branch = await git.currentBranch(); 31 | 32 | // Check branch 33 | if (branch === 'main' || branch === 'release' || branch === 'dev') { 34 | error(`Rebase of branch ${branch} is not allowed!`); 35 | return; 36 | } 37 | 38 | // Ask to Rebase the branch 39 | if (!parameters.options.noConfirm && !(await confirm(`Rebase branch ${branch}?`))) { 40 | return; 41 | } 42 | 43 | // Select base branch 44 | let baseBranch = parameters.first; 45 | if (!baseBranch || !(await git.getBranch(baseBranch))) { 46 | baseBranch = await git.selectBranch({ text: 'Select base branch' }); 47 | } 48 | 49 | // Start timer 50 | const timer = startTimer(); 51 | 52 | // Rebase 53 | const rebaseSpin = spin(`Set ${baseBranch} as base of ${branch}`); 54 | await run( 55 | `git fetch && git checkout ${baseBranch} && git pull && git checkout ${branch} && git rebase ${baseBranch}`, 56 | ); 57 | rebaseSpin.succeed(); 58 | 59 | // Success 60 | success(`Rebased ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 61 | info(''); 62 | 63 | // For tests 64 | return `rebased ${branch}`; 65 | }, 66 | }; 67 | 68 | export default NewCommand; 69 | -------------------------------------------------------------------------------- /src/commands/git/rename.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Rename branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['rn'], 10 | description: 'Rename branch', 11 | hidden: false, 12 | name: 'rename', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { error, info, spin, success }, 20 | prompt: { confirm }, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Get current branch 30 | const branch = await git.currentBranch(); 31 | 32 | // Get new name 33 | const name = await helper.getInput(parameters.first, { 34 | name: 'new name', 35 | showError: true, 36 | }); 37 | if (!branch) { 38 | return; 39 | } 40 | 41 | // Check branch 42 | if (branch === 'main' || branch === 'release' || branch === 'dev') { 43 | error(`Rename branch ${branch} is not allowed!`); 44 | return; 45 | } 46 | 47 | // Check name 48 | if (await git.getBranch(name, { exact: true })) { 49 | error(`Branch with name ${name} already exists`); 50 | return; 51 | } 52 | 53 | // Ask to rename branch 54 | if (!parameters.options.noConfirm && !(await confirm(`Rename branch ${branch} into ${name}?`))) { 55 | return; 56 | } 57 | 58 | // Start timer 59 | let timer = startTimer(); 60 | 61 | // Get remote 62 | const remote = await git.getBranch(name, { exact: true, remote: true }); 63 | 64 | // Rename branch 65 | const renameSpin = spin(`Rename ${branch} into ${name}`); 66 | await run(`git branch -m ${name}`); 67 | 68 | // Ask to push branch 69 | if (remote && (parameters.options.noConfirm || (await confirm(`Push ${name} to remote?`)))) { 70 | await run(`git push origin ${name}`); 71 | } 72 | renameSpin.succeed(); 73 | 74 | // Save time 75 | let time = timer(); 76 | 77 | // Ask to delete remote branch 78 | if ( 79 | remote 80 | && (parameters.options.deleteRemote 81 | || (!parameters.options.noConfirm && (await confirm(`Delete remote branch ${branch}?`)))) 82 | ) { 83 | timer = startTimer(); 84 | const deleteSpin = spin(`Delete remote branch ${branch}`); 85 | await run(`git push origin :${branch}`); 86 | deleteSpin.succeed(); 87 | time += timer(); 88 | } 89 | 90 | // Success 91 | success(`Renamed ${branch} to ${name} in ${helper.msToMinutesAndSeconds(time)}m.`); 92 | info(''); 93 | 94 | // For tests 95 | return `renamed ${branch} to ${name}`; 96 | }, 97 | }; 98 | 99 | export default NewCommand; 100 | -------------------------------------------------------------------------------- /src/commands/git/reset.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Reset current branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['rs'], 10 | description: 'Reset current branch', 11 | hidden: false, 12 | name: 'reset', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | npm, 19 | parameters, 20 | print: { error, info, spin, success }, 21 | prompt, 22 | system, 23 | } = toolbox; 24 | 25 | // Start timer 26 | const timer = system.startTimer(); 27 | 28 | // Check git 29 | if (!(await git.gitInstalled())) { 30 | return; 31 | } 32 | 33 | // Current branch 34 | const branch = await git.currentBranch(); 35 | if (!branch) { 36 | error('No current branch!'); 37 | return; 38 | } 39 | 40 | // Check remote 41 | const remoteBranch = await system.run(`git ls-remote --heads origin ${branch}`); 42 | if (!remoteBranch) { 43 | error(`No remote branch ${branch} found!`); 44 | return; 45 | } 46 | 47 | // Ask for reset 48 | if (!parameters.options.noConfirm && !(await prompt.confirm(`Reset branch ${branch} to the remote state`))) { 49 | return; 50 | } 51 | 52 | // Reset 53 | const resetSpin = spin(`Reset ${branch}`); 54 | await system.run( 55 | 'git clean -fd && ' 56 | + 'git reset HEAD --hard && ' 57 | + 'git checkout main && ' 58 | + 'git fetch && ' 59 | + 'git pull && ' 60 | + `git branch -D ${ 61 | branch 62 | } && ` 63 | + `git checkout ${ 64 | branch 65 | } && ` 66 | + 'git pull', 67 | ); 68 | resetSpin.succeed(); 69 | 70 | // Install npm packages 71 | await npm.install(); 72 | 73 | // Success info 74 | success(`Branch ${branch} was reset in in ${helper.msToMinutesAndSeconds(timer())}m.`); 75 | info(''); 76 | 77 | // For tests 78 | return `reset branch ${branch}`; 79 | }, 80 | }; 81 | 82 | export default NewCommand; 83 | -------------------------------------------------------------------------------- /src/commands/git/squash.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Squash branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['s'], 10 | description: 'Squash branch', 11 | hidden: false, 12 | name: 'squash', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { error, info, spin, success }, 20 | prompt: { ask, confirm }, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Get current branch 30 | const branch = await git.currentBranch(); 31 | 32 | // Check branch 33 | if (['dev', 'develop', 'main', 'master', 'release', 'test'].includes(branch)) { 34 | error(`Squash of branch ${branch} is not allowed!`); 35 | return; 36 | } 37 | 38 | // Check for changes 39 | if (await git.changes({ showError: true })) { 40 | return; 41 | } 42 | 43 | // Ask to squash the branch 44 | if (!parameters.options.noConfirm && !(await confirm(`Squash branch ${branch}?`))) { 45 | return; 46 | } 47 | 48 | // Start timer 49 | const timer = startTimer(); 50 | 51 | // Get description 52 | const base = await helper.getInput(parameters.first, { 53 | initial: 'dev', 54 | name: 'Base branche', 55 | showError: false, 56 | }); 57 | 58 | // Merge base 59 | const mergeBaseSpin = spin(`Get merge ${base}`); 60 | const mergeBase = await git.getMergeBase(base); 61 | if (!mergeBase) { 62 | error('No merge base found!'); 63 | return; 64 | } 65 | mergeBaseSpin.succeed(); 66 | 67 | // Get squash message (before reset) 68 | const squashMessage = await git.getFirstBranchCommit(await git.currentBranch(), base); 69 | 70 | // Soft reset 71 | const resetSpin = spin('Soft reset'); 72 | await git.reset(mergeBase, true); 73 | resetSpin.succeed(); 74 | 75 | // Get status 76 | const status = await git.status(); 77 | 78 | // Ask to go on 79 | info(`You are now on commit ${mergeBase}, with following changes:`); 80 | info(status); 81 | if (!parameters.options.noConfirm && !(await confirm('Continue?'))) { 82 | return; 83 | } 84 | 85 | // Get git user 86 | const user = await git.getUser(); 87 | 88 | // Ask for author 89 | let author = parameters.options.author; 90 | if (!author) { 91 | author = ( 92 | await ask({ 93 | initial: `${user.name} <${user.email}>`, 94 | message: 'Author: ', 95 | name: 'author', 96 | type: 'input', 97 | }) 98 | ).author; 99 | } 100 | 101 | // Ask for message 102 | let message = parameters.options.message; 103 | if (!message) { 104 | message = ( 105 | await ask({ 106 | initial: squashMessage, 107 | message: 'Message: ', 108 | name: 'message', 109 | type: 'input', 110 | }) 111 | ).message; 112 | } 113 | 114 | // Confirm inputs 115 | info(author); 116 | info(message); 117 | if (!parameters.options.noConfirm && !(await confirm('Commit?'))) { 118 | return; 119 | } 120 | 121 | // Start spinner 122 | const commitSpin = spin('Commit'); 123 | 124 | // Commit and push 125 | await run(`git commit -am "${message}" --author="${author}"`); 126 | commitSpin.succeed(); 127 | 128 | if (!parameters.options.noConfirm && !(await confirm('Push force?'))) { 129 | return; 130 | } 131 | 132 | // Start timer 133 | const pushForceSpin = spin('Push force'); 134 | 135 | // Push 136 | await run('git push -f origin HEAD'); 137 | pushForceSpin.succeed(); 138 | 139 | // Success 140 | success(`Squashed ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 141 | info(''); 142 | if (!toolbox.parameters.options.fromGluegunMenu) { 143 | process.exit(); 144 | } 145 | 146 | // For tests 147 | return `squashed ${branch}`; 148 | }, 149 | }; 150 | 151 | export default NewCommand; 152 | -------------------------------------------------------------------------------- /src/commands/git/undo.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Undo last commit (without loosing files) 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['un'], 10 | description: 'Undo last commit (without loosing files)', 11 | hidden: false, 12 | name: 'undo', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | parameters, 19 | print: { info, spin, success }, 20 | prompt: { confirm }, 21 | system: { run, startTimer }, 22 | } = toolbox; 23 | 24 | // Check git 25 | if (!(await git.gitInstalled())) { 26 | return; 27 | } 28 | 29 | // Last commit message 30 | const lastCommitMessage = await git.lastCommitMessage(); 31 | 32 | // Ask to squash the branch 33 | if (!parameters.options.noConfirm && !(await confirm(`Undo last commit "${lastCommitMessage}"?`))) { 34 | return; 35 | } 36 | 37 | // Start timer 38 | const timer = startTimer(); 39 | 40 | // Get current branch 41 | const branch = await git.currentBranch(); 42 | 43 | // Reset soft 44 | const undoSpinner = spin(`Undo last commit of branch ${branch}`); 45 | await run('git reset --soft HEAD~'); 46 | undoSpinner.succeed(); 47 | 48 | // Success 49 | success(`Undo last commit of ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 50 | info(''); 51 | 52 | if (!toolbox.parameters.options.fromGluegunMenu) { 53 | process.exit(); 54 | } 55 | 56 | // For tests 57 | return `undo last commit of branch ${branch}`; 58 | }, 59 | }; 60 | 61 | export default NewCommand; 62 | -------------------------------------------------------------------------------- /src/commands/git/update.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Update branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['up'], 10 | description: 'Update branch', 11 | hidden: false, 12 | name: 'update', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | git, 17 | helper, 18 | npm, 19 | print: { info, spin, success }, 20 | system: { run, startTimer }, 21 | } = toolbox; 22 | 23 | // Check git 24 | if (!(await git.gitInstalled())) { 25 | return; 26 | } 27 | 28 | // Start timer 29 | const timer = startTimer(); 30 | 31 | // Get current branch 32 | const branch = await git.currentBranch(); 33 | 34 | // Update 35 | const updateSpin = spin(`Update branch ${branch}`); 36 | await run('git fetch && git pull'); 37 | updateSpin.succeed(); 38 | 39 | // Install npm packages 40 | await npm.install(); 41 | 42 | // Success 43 | success(`Updated ${branch} in ${helper.msToMinutesAndSeconds(timer())}m.`); 44 | info(''); 45 | 46 | // For tests 47 | return `updated ${branch}`; 48 | }, 49 | }; 50 | 51 | export default NewCommand; 52 | -------------------------------------------------------------------------------- /src/commands/lt.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Welcome to lenne.Tech CLI 5 | */ 6 | module.exports = { 7 | description: 'Welcome to lenne.Tech CLI', 8 | hidden: true, 9 | name: 'lt', 10 | run: async (toolbox: ExtendedGluegunToolbox) => { 11 | await toolbox.helper.showMenu(null, { headline: `Welcome to lenne.Tech CLI ${toolbox.meta.version()}` }); 12 | return 'lt'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/commands/npm/npm.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Npm commands 5 | */ 6 | module.exports = { 7 | alias: ['n'], 8 | description: 'Npm commands', 9 | hidden: true, 10 | name: 'npm', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('npm'); 13 | return 'npm'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/npm/reinit.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { dirname } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Reinitialize npm packages 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['r'], 11 | description: 'Reinitialize npm packages', 12 | hidden: false, 13 | name: 'reinit', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | helper, 18 | npm, 19 | parameters, 20 | print: { spin, success }, 21 | prompt, 22 | system, 23 | } = toolbox; 24 | 25 | // Start timer 26 | const timer = system.startTimer(); 27 | 28 | // Check 29 | const { data, path } = await npm.getPackageJson({ showError: true }); 30 | if (!path) { 31 | return; 32 | } 33 | 34 | // Update packages 35 | let update = parameters.options.update || parameters.options.u; 36 | if (!update && !parameters.options.noConfirm) { 37 | update = await prompt.confirm('Update package.json before reinitialization?'); 38 | } 39 | if (update) { 40 | if (!system.which('ncu')) { 41 | const installSpin = spin('Install ncu'); 42 | await system.run('npm i -g npm-check-updates'); 43 | installSpin.succeed(); 44 | } 45 | const updateSpin = spin('Update package.json'); 46 | await system.run(`ncu -u --packageFile ${path}`); 47 | updateSpin.succeed(); 48 | } 49 | 50 | // Reinitialize 51 | 52 | if (data.scripts && data.scripts.reinit) { 53 | const ownReinitSpin = spin('Reinitialize npm packages'); 54 | await system.run(`cd ${dirname(path)} && npm run reinit`); 55 | ownReinitSpin.succeed(); 56 | } else { 57 | const reinitSpin = spin('Reinitialize npm packages'); 58 | if (system.which('rimraf')) { 59 | await system.run('npm i -g rimraf'); 60 | } 61 | await system.run( 62 | `cd ${dirname(path)} && rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm i`, 63 | ); 64 | reinitSpin.succeed(); 65 | if (data.scripts && data.scripts['test:e2e']) { 66 | const testE2eSpin = spin('Run tests'); 67 | await system.run(`cd ${dirname(path)} && npm run test:e2e`); 68 | testE2eSpin.succeed(); 69 | } else if (data.scripts && data.scripts && data.scripts.test) { 70 | const testSpin = spin('Run tests'); 71 | await system.run(`cd ${dirname(path)} && npm run test`); 72 | testSpin.succeed(); 73 | } 74 | } 75 | 76 | // Success info 77 | success(`Reinitialized npm packages in ${helper.msToMinutesAndSeconds(timer())}m.`); 78 | 79 | // For tests 80 | return 'npm reinit'; 81 | }, 82 | }; 83 | 84 | export default NewCommand; 85 | -------------------------------------------------------------------------------- /src/commands/npm/update.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Update npm packages 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['u', 'up'], 10 | description: 'Update npm packages', 11 | hidden: false, 12 | name: 'update', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | helper, 17 | npm, 18 | print: { success }, 19 | system, 20 | } = toolbox; 21 | 22 | // Start timer 23 | const timer = system.startTimer(); 24 | 25 | // Update 26 | await npm.update({ install: true, showError: true }); 27 | 28 | // Success info 29 | success(`Updated npm packages in ${helper.msToMinutesAndSeconds(timer())}m.`); 30 | 31 | // For tests 32 | return 'npm update'; 33 | }, 34 | }; 35 | 36 | export default NewCommand; 37 | -------------------------------------------------------------------------------- /src/commands/server/create-secret.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import { GluegunCommand } from 'gluegun'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Open regex tools in browser 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['cs'], 11 | description: 'Create a new secret string (for JWT config)', 12 | hidden: false, 13 | name: 'createSecret', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | const { 16 | print: { success }, 17 | } = toolbox; 18 | success(crypto.randomBytes(512).toString('base64')); 19 | 20 | // For tests 21 | return 'secret created'; 22 | }, 23 | }; 24 | 25 | export default NewCommand; 26 | -------------------------------------------------------------------------------- /src/commands/server/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new server 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['c'], 10 | description: 'Creates a new server', 11 | hidden: false, 12 | name: 'create', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | git, 18 | helper, 19 | meta, 20 | parameters, 21 | patching, 22 | print: { error, info, spin, success }, 23 | prompt: { confirm }, 24 | server, 25 | strings: { kebabCase }, 26 | system, 27 | template, 28 | } = toolbox; 29 | 30 | // Start timer 31 | const timer = system.startTimer(); 32 | 33 | // Info 34 | info('Create a new server'); 35 | 36 | // Check git 37 | if (!(await git.gitInstalled())) { 38 | return; 39 | } 40 | 41 | // Get name 42 | const name = await helper.getInput(parameters.first, { 43 | name: 'server name', 44 | showError: true, 45 | }); 46 | if (!name) { 47 | return; 48 | } 49 | 50 | // Set project directory 51 | const projectDir = kebabCase(name); 52 | 53 | // Check if directory already exists 54 | if (filesystem.exists(projectDir)) { 55 | info(''); 56 | error(`There's already a folder named "${projectDir}" here.`); 57 | return undefined; 58 | } 59 | 60 | // Clone git repository 61 | const cloneSpinner = spin('Clone https://github.com/lenneTech/nest-server-starter.git'); 62 | await system.run(`git clone https://github.com/lenneTech/nest-server-starter.git ${projectDir}`); 63 | if (filesystem.isDirectory(`./${projectDir}`)) { 64 | filesystem.remove(`./${projectDir}/.git`); 65 | cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/nest-server-starter.git'); 66 | } 67 | 68 | // Check directory 69 | if (!filesystem.isDirectory(`./${projectDir}`)) { 70 | error(`The directory "${projectDir}" could not be created.`); 71 | return undefined; 72 | } 73 | 74 | // Get description 75 | const description = await helper.getInput(parameters.second, { 76 | name: 'Description', 77 | showError: false, 78 | }); 79 | 80 | // Get author 81 | const author = await helper.getInput(parameters.second, { 82 | name: 'Author', 83 | showError: false, 84 | }); 85 | 86 | const prepareSpinner = spin('Prepare files'); 87 | 88 | // Set readme 89 | await template.generate({ 90 | props: { description, name }, 91 | target: `./${projectDir}/README.md`, 92 | template: 'nest-server-starter/README.md.ejs', 93 | }); 94 | 95 | // Replace secret or private keys and remove `nest-server` 96 | await patching.update(`./${projectDir}/src/config.env.ts`, content => server.replaceSecretOrPrivateKeys(content).replace(/nest-server-/g, `${projectDir 97 | }-`)); 98 | 99 | // Set package.json 100 | await patching.update(`./${projectDir}/package.json`, (config) => { 101 | config.author = author; 102 | config.bugs = { 103 | url: '', 104 | }; 105 | config.description = description || name; 106 | config.homepage = ''; 107 | config.name = projectDir; 108 | config.repository = { 109 | type: 'git', 110 | url: '', 111 | }; 112 | config.version = '0.0.1'; 113 | return config; 114 | }); 115 | 116 | // Set package.json 117 | if (filesystem.exists(`./${projectDir}/src/meta`)) { 118 | await patching.update(`./${projectDir}/src/meta`, (config) => { 119 | config.name = name; 120 | config.description = description; 121 | return config; 122 | }); 123 | } 124 | 125 | prepareSpinner.succeed('Files prepared'); 126 | 127 | // Init 128 | const installSpinner = spin('Install npm packages'); 129 | await system.run(`cd ${projectDir} && npm i`); 130 | installSpinner.succeed('NPM packages installed'); 131 | if (git) { 132 | const inGit = (await system.run('git rev-parse --is-inside-work-tree'))?.trim(); 133 | if (inGit !== 'true') { 134 | const initializeGit = await confirm('Initialize git?', true); 135 | if (initializeGit) { 136 | const initGitSpinner = spin('Initialize git'); 137 | await system.run( 138 | `cd ${projectDir} && git init && git add . && git commit -am "Init via lenne.Tech CLI ${meta.version()}"`, 139 | ); 140 | initGitSpinner.succeed('Git initialized'); 141 | } 142 | } 143 | } 144 | 145 | // We're done, so show what to do next 146 | info(''); 147 | success( 148 | `Generated ${name} server with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`, 149 | ); 150 | info(''); 151 | info('Next:'); 152 | info(' Start database server (e.g. MongoDB)'); 153 | info(` Check config: ${projectDir}/src/config.env.ts`); 154 | info(` Go to project directory: cd ${projectDir}`); 155 | info(' Run tests: npm run test:e2e'); 156 | info(' Start server: npm start'); 157 | info(''); 158 | 159 | if (!toolbox.parameters.options.fromGluegunMenu) { 160 | process.exit(); 161 | } 162 | 163 | // For tests 164 | return `new server ${name}`; 165 | }, 166 | }; 167 | 168 | export default NewCommand; 169 | -------------------------------------------------------------------------------- /src/commands/server/module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { ExtendedGluegunCommand } from '../../interfaces/extended-gluegun-command'; 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | import genObject from './object'; 6 | 7 | /** 8 | * Create a new server module 9 | */ 10 | const NewCommand: ExtendedGluegunCommand = { 11 | alias: ['m'], 12 | description: 'Creates a new server module', 13 | hidden: false, 14 | name: 'module', 15 | run: async ( 16 | toolbox: ExtendedGluegunToolbox, 17 | options?: { 18 | currentItem?: string; 19 | objectsToAdd?: { object: string; property: string }[]; 20 | preventExitProcess?: boolean; 21 | referencesToAdd?: { property: string; reference: string }[]; 22 | }, 23 | ) => { 24 | 25 | // Options: 26 | const { currentItem, objectsToAdd, preventExitProcess, referencesToAdd } = { 27 | currentItem: '', 28 | objectsToAdd: [], 29 | preventExitProcess: false, 30 | referencesToAdd: [], 31 | ...options, 32 | }; 33 | 34 | // Retrieve the tools we need 35 | const { 36 | filesystem, 37 | helper, 38 | parameters, 39 | patching, 40 | print: { divider, error, info, spin, success }, 41 | prompt: { ask, confirm }, 42 | server, 43 | strings: { camelCase, kebabCase, pascalCase }, 44 | system, 45 | template, 46 | } = toolbox; 47 | 48 | // Start timer 49 | const timer = system.startTimer(); 50 | 51 | // Info 52 | if (currentItem) { 53 | info(`Creating a new server module for ${currentItem}`); 54 | } else { 55 | info('Create a new server module'); 56 | } 57 | 58 | const name = await helper.getInput(currentItem || parameters.first, { 59 | initial: currentItem || '', 60 | name: 'module name', 61 | }); 62 | if (!name) { 63 | return; 64 | } 65 | 66 | 67 | const controller = (await ask({ 68 | choices: ['Rest', 'GraphQL', 'Both'], 69 | message: 'What controller type?', 70 | name: 'controller', 71 | type: 'select', 72 | })).controller; 73 | 74 | // Set up initial props (to pass into templates) 75 | const nameCamel = camelCase(name); 76 | const nameKebab = kebabCase(name); 77 | const namePascal = pascalCase(name); 78 | 79 | // Check if directory 80 | const cwd = filesystem.cwd(); 81 | const path = cwd.substr(0, cwd.lastIndexOf('src')); 82 | if (!filesystem.exists(join(path, 'src'))) { 83 | info(''); 84 | error(`No src directory in "${path}".`); 85 | return undefined; 86 | } 87 | const directory = join(path, 'src', 'server', 'modules', nameKebab); 88 | if (filesystem.exists(directory)) { 89 | info(''); 90 | error(`Module directory "${directory}" already exists.`); 91 | return undefined; 92 | } 93 | 94 | const { props, refsSet, schemaSet } = await server.addProperties({ objectsToAdd, referencesToAdd }); 95 | 96 | const generateSpinner = spin('Generate files'); 97 | const declare = server.useDefineForClassFieldsActivated(); 98 | const inputTemplate = server.propsForInput(props, { declare, modelName: name, nullable: true }); 99 | const createTemplate = server.propsForInput(props, { create: true, declare, modelName: name, nullable: false }); 100 | const modelTemplate = server.propsForModel(props, { declare, modelName: name }); 101 | 102 | // nest-server-module/inputs/xxx.input.ts 103 | await template.generate({ 104 | props: { imports: inputTemplate.imports, nameCamel, nameKebab, namePascal, props: inputTemplate.props }, 105 | target: join(directory, 'inputs', `${nameKebab}.input.ts`), 106 | template: 'nest-server-module/inputs/template.input.ts.ejs', 107 | }); 108 | 109 | if (controller === 'Rest' || controller === 'Both') { 110 | await template.generate({ 111 | props: { lowercase: name.toLowerCase(), nameCamel: camelCase(name), nameKebab: kebabCase(name), namePascal: pascalCase(name) }, 112 | target: join(directory, `${nameKebab}.controller.ts`), 113 | template: 'nest-server-module/template.controller.ts.ejs', 114 | }); 115 | } 116 | 117 | // nest-server-module/inputs/xxx-create.input.ts 118 | await template.generate({ 119 | props: { imports: createTemplate.imports, isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal, props: createTemplate.props }, 120 | target: join(directory, 'inputs', `${nameKebab}-create.input.ts`), 121 | template: 'nest-server-module/inputs/template-create.input.ts.ejs', 122 | }); 123 | 124 | // nest-server-module/output/find-and-count-xxxs-result.output.ts 125 | await template.generate({ 126 | props: { isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal }, 127 | target: join(directory, 'outputs', `find-and-count-${nameKebab}s-result.output.ts`), 128 | template: 'nest-server-module/outputs/template-fac-result.output.ts.ejs', 129 | }); 130 | 131 | // nest-server-module/xxx.model.ts 132 | await template.generate({ 133 | props: { 134 | imports: modelTemplate.imports, 135 | isGql: controller === 'GraphQL' || controller === 'Both', 136 | mappings: modelTemplate.mappings, 137 | nameCamel, 138 | nameKebab, 139 | namePascal, 140 | props: modelTemplate.props, 141 | }, 142 | target: join(directory, `${nameKebab}.model.ts`), 143 | template: 'nest-server-module/template.model.ts.ejs', 144 | }); 145 | 146 | // nest-server-module/xxx.module.ts 147 | await template.generate({ 148 | props: { controller, nameCamel, nameKebab, namePascal }, 149 | target: join(directory, `${nameKebab}.module.ts`), 150 | template: 'nest-server-module/template.module.ts.ejs', 151 | }); 152 | 153 | if (controller === 'GraphQL' || controller === 'Both') { 154 | // nest-server-module/xxx.resolver.ts 155 | await template.generate({ 156 | props: { nameCamel, nameKebab, namePascal }, 157 | target: join(directory, `${nameKebab}.resolver.ts`), 158 | template: 'nest-server-module/template.resolver.ts.ejs', 159 | }); 160 | } 161 | 162 | // nest-server-module/xxx.service.ts 163 | await template.generate({ 164 | props: { nameCamel, nameKebab, namePascal }, 165 | target: join(directory, `${nameKebab}.service.ts`), 166 | template: 'nest-server-module/template.service.ts.ejs', 167 | }); 168 | 169 | generateSpinner.succeed('Files generated'); 170 | 171 | const serverModule = join(path, 'src', 'server', 'server.module.ts'); 172 | if (filesystem.exists(serverModule)) { 173 | const includeSpinner = spin('Include module into server'); 174 | 175 | // Import module 176 | await patching.patch(serverModule, { 177 | before: 'import', 178 | insert: `import { ${namePascal}Module } from './modules/${nameKebab}/${nameKebab}.module';\n`, 179 | }); 180 | 181 | // Add Module directly into imports config 182 | const patched = await patching.patch(serverModule, { 183 | after: new RegExp('imports:[^\\]]*', 'm'), 184 | insert: ` ${namePascal}Module,\n `, 185 | }); 186 | 187 | // Add Module with forwardRef in exported imports 188 | if (!patched) { 189 | await patching.patch(serverModule, { 190 | after: new RegExp('imports = \\[[^\\]]*', 'm'), 191 | insert: ` forwardRef(() => ${namePascal}Module),\n `, 192 | }); 193 | } 194 | 195 | // Add comma if necessary 196 | await patching.patch(serverModule, { 197 | insert: '$1,$2', 198 | replace: new RegExp(`([^,\\s])(\\s*${namePascal}Module,\\s*\\])`), 199 | }); 200 | 201 | includeSpinner.succeed('Module included'); 202 | } else { 203 | info('Don\'t forget to include the module into your main module.'); 204 | } 205 | 206 | // Linting 207 | // if (await confirm('Run lint?', false)) { 208 | // await system.run('npm run lint'); 209 | // } 210 | 211 | // We're done, so show what to do next 212 | info(''); 213 | success(`Generated ${namePascal}Module in ${helper.msToMinutesAndSeconds(timer())}m.`); 214 | info(''); 215 | 216 | // Add additional references 217 | if (referencesToAdd.length > 0) { 218 | divider(); 219 | const nextRef = referencesToAdd.shift().reference; 220 | await NewCommand.run(toolbox, { currentItem: nextRef, objectsToAdd, preventExitProcess: true, referencesToAdd }); 221 | } 222 | 223 | // Add additional objects 224 | if (objectsToAdd.length > 0) { 225 | divider(); 226 | const nextObj = objectsToAdd.shift().object; 227 | await genObject.run(toolbox, { currentItem: nextObj, objectsToAdd, preventExitProcess: true, referencesToAdd }); 228 | } 229 | 230 | // Lint fix 231 | if (await confirm('Run lint fix?', true)) { 232 | await system.run('npm run lint:fix'); 233 | } 234 | 235 | divider(); 236 | 237 | // We're done, so show what to do next 238 | if (!preventExitProcess) { 239 | if (refsSet || schemaSet) { 240 | success('HINT: References / Schemata have been added, so it is necessary to add the corresponding imports!'); 241 | } 242 | 243 | if (!toolbox.parameters.options.fromGluegunMenu) { 244 | process.exit(); 245 | } 246 | } 247 | 248 | // For tests 249 | return `new module ${name}`; 250 | }, 251 | }; 252 | 253 | export default NewCommand; 254 | -------------------------------------------------------------------------------- /src/commands/server/object.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { ExtendedGluegunCommand } from '../../interfaces/extended-gluegun-command'; 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | import genModule from './module'; 6 | 7 | /** 8 | * Create a new server module 9 | */ 10 | const NewCommand: ExtendedGluegunCommand = { 11 | alias: ['o'], 12 | description: 'Creates a new server object (with inputs)', 13 | hidden: false, 14 | name: 'object', 15 | run: async ( 16 | toolbox: ExtendedGluegunToolbox, 17 | options?: { 18 | currentItem?: string; 19 | objectsToAdd?: { object: string; property: string }[]; 20 | preventExitProcess?: boolean; 21 | referencesToAdd?: { property: string; reference: string }[]; 22 | }, 23 | ) => { 24 | 25 | // Options: 26 | const { currentItem, objectsToAdd, preventExitProcess, referencesToAdd } = { 27 | currentItem: '', 28 | objectsToAdd: [], 29 | preventExitProcess: false, 30 | referencesToAdd: [], 31 | ...options, 32 | }; 33 | 34 | // Retrieve the tools we need 35 | const { 36 | filesystem, 37 | helper, 38 | parameters, 39 | print: { divider, error, info, spin, success }, 40 | prompt: { confirm }, 41 | server, 42 | strings: { camelCase, kebabCase, pascalCase }, 43 | system, 44 | template, 45 | } = toolbox; 46 | 47 | // Start timer 48 | const timer = system.startTimer(); 49 | 50 | // Info 51 | if (currentItem) { 52 | info(`Create a new server object (with inputs) for ${currentItem}`); 53 | } else { 54 | info('Create a new server object (with inputs)'); 55 | } 56 | 57 | // Get name 58 | const name = await helper.getInput(currentItem || parameters.first, { 59 | initial: currentItem || '', 60 | name: 'object name', 61 | }); 62 | if (!name) { 63 | return; 64 | } 65 | 66 | // Set up initial props (to pass into templates) 67 | const nameCamel = camelCase(name); 68 | const nameKebab = kebabCase(name); 69 | const namePascal = pascalCase(name); 70 | 71 | // Check if directory 72 | const cwd = filesystem.cwd(); 73 | const path = cwd.substr(0, cwd.lastIndexOf('src')); 74 | if (!filesystem.exists(join(path, 'src'))) { 75 | info(''); 76 | error(`No src directory in "${path}".`); 77 | return undefined; 78 | } 79 | const directory = join(path, 'src', 'server', 'common', 'objects', nameKebab); 80 | if (filesystem.exists(directory)) { 81 | info(''); 82 | error(`Module directory "${directory}" already exists.`); 83 | return undefined; 84 | } 85 | 86 | const { props, refsSet, schemaSet } = await server.addProperties({ objectsToAdd, referencesToAdd }); 87 | 88 | const generateSpinner = spin('Generate files'); 89 | const declare = server.useDefineForClassFieldsActivated(); 90 | const inputTemplate = server.propsForInput(props, { declare, modelName: name, nullable: true }); 91 | const createTemplate = server.propsForInput(props, { create: true, declare, modelName: name, nullable: false }); 92 | const objectTemplate = server.propsForModel(props, { declare, modelName: name }); 93 | 94 | // nest-server-module/inputs/xxx.input.ts 95 | await template.generate({ 96 | props: { imports: inputTemplate.imports, nameCamel, nameKebab, namePascal, props: inputTemplate.props }, 97 | target: join(directory, `${nameKebab}.input.ts`), 98 | template: 'nest-server-object/template.input.ts.ejs', 99 | }); 100 | 101 | // nest-server-object/inputs/xxx-create.input.ts 102 | await template.generate({ 103 | props: { imports: createTemplate.imports, nameCamel, nameKebab, namePascal, props: createTemplate.props }, 104 | target: join(directory, `${nameKebab}-create.input.ts`), 105 | template: 'nest-server-object/template-create.input.ts.ejs', 106 | }); 107 | 108 | // nest-server-module/xxx.model.ts 109 | await template.generate({ 110 | props: { 111 | imports: objectTemplate.imports, 112 | mappings: objectTemplate.mappings, 113 | nameCamel, 114 | nameKebab, 115 | namePascal, 116 | props: objectTemplate.props, 117 | }, 118 | target: join(directory, `${nameKebab}.object.ts`), 119 | template: 'nest-server-object/template.object.ts.ejs', 120 | }); 121 | 122 | generateSpinner.succeed('Files generated'); 123 | 124 | // Lint fix 125 | if (await confirm('Run lint fix?', true)) { 126 | await system.run('npm run lint:fix'); 127 | } 128 | 129 | // We're done, so show what to do next 130 | info(''); 131 | success(`Generated ${namePascal}Object in ${helper.msToMinutesAndSeconds(timer())}m.`); 132 | info(''); 133 | 134 | // Add additional objects 135 | if (objectsToAdd.length > 0) { 136 | divider(); 137 | const nextObj = objectsToAdd.shift().object; 138 | await NewCommand.run(toolbox, { currentItem: nextObj, objectsToAdd, preventExitProcess: true, referencesToAdd }); 139 | } 140 | 141 | // Add additional references 142 | if (referencesToAdd.length > 0) { 143 | divider(); 144 | const nextRef = referencesToAdd.shift().reference; 145 | await genModule.run(toolbox, { currentItem: nextRef, objectsToAdd, preventExitProcess: true, referencesToAdd }); 146 | } 147 | 148 | // We're done, so show what to do next 149 | if (!preventExitProcess) { 150 | if (refsSet || schemaSet) { 151 | success('HINT: References / Schemata have been added, so it is necessary to add the corresponding imports!'); 152 | } 153 | 154 | if (!toolbox.parameters.options.fromGluegunMenu) { 155 | process.exit(); 156 | } 157 | } 158 | 159 | // For tests 160 | return `new object ${name}`; 161 | }, 162 | }; 163 | 164 | export default NewCommand; 165 | -------------------------------------------------------------------------------- /src/commands/server/server.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Server commands 5 | */ 6 | module.exports = { 7 | alias: ['s'], 8 | description: 'Server commands', 9 | hidden: true, 10 | name: 'server', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('server'); 13 | return 'server'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/server/set-secrets.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Set secrets for the server configuration 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['scs'], 10 | description: 'Set secrets for the server configuration', 11 | hidden: false, 12 | name: 'setConfigSecrets', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | parameters, 18 | patching, 19 | print: { error, info, spin }, 20 | server, 21 | } = toolbox; 22 | 23 | // Check if file exists 24 | const filePath = parameters.first || 'src/config.env.ts'; 25 | if (!filesystem.exists(filePath)) { 26 | info(''); 27 | error(`There's no file named "${filePath}"`); 28 | return undefined; 29 | } 30 | 31 | // Set secrets 32 | const prepareSpinner = spin(`Setting secrets in server configuration: ${filePath}`); 33 | await patching.update(filePath, content => server.replaceSecretOrPrivateKeys(content)); 34 | prepareSpinner.succeed(`Secrets set in server configuration ${filePath}`); 35 | 36 | // For tests 37 | return 'secrets in server configuration set'; 38 | }, 39 | }; 40 | 41 | export default NewCommand; 42 | -------------------------------------------------------------------------------- /src/commands/server/test.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { join } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Create a new server 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['t'], 11 | description: 'Creates a new test file', 12 | hidden: false, 13 | name: 'test', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | filesystem, 18 | helper, 19 | parameters, 20 | print: { error, info, spin, success }, 21 | strings: { camelCase, kebabCase, pascalCase }, 22 | system, 23 | template, 24 | } = toolbox; 25 | 26 | // Start timer 27 | const timer = system.startTimer(); 28 | 29 | // Info 30 | info('Create a new test file'); 31 | 32 | // Get name 33 | const name = await helper.getInput(parameters.first, { 34 | name: 'test name', 35 | }); 36 | if (!name) { 37 | return; 38 | } 39 | 40 | // Set up initial props (to pass into templates) 41 | const nameCamel = camelCase(name); 42 | const nameKebab = kebabCase(name); 43 | const namePascal = pascalCase(name); 44 | 45 | // Check if directory 46 | const cwd = filesystem.cwd(); 47 | const path = cwd.substr(0, cwd.lastIndexOf('src')); 48 | if (!filesystem.exists(join(path, 'tests'))) { 49 | info(''); 50 | error(`No tests directory in "${path}".`); 51 | return undefined; 52 | } 53 | const testsDir = join(path, 'tests'); 54 | const filePath = join(testsDir, `${nameKebab}.e2e-spec.ts`); 55 | 56 | // Check if file already exists 57 | if (filesystem.exists(filePath)) { 58 | info(''); 59 | error(`There's already a file named "${filePath}"`); 60 | return undefined; 61 | } 62 | 63 | const generateSpinner = spin('Generate test file'); 64 | 65 | // nest-server-tests/tests.e2e-spec.ts.ejs 66 | await template.generate({ 67 | props: { nameCamel, nameKebab, namePascal }, 68 | target: filePath, 69 | template: 'nest-server-tests/tests.e2e-spec.ts.ejs', 70 | }); 71 | 72 | generateSpinner.succeed('Generate test file'); 73 | 74 | // We're done, so show what to do next 75 | info(''); 76 | success(`Generated ${namePascal} test file in ${helper.msToMinutesAndSeconds(timer())}m.`); 77 | info(''); 78 | 79 | if (!toolbox.parameters.options.fromGluegunMenu) { 80 | process.exit(); 81 | } 82 | 83 | // For tests 84 | return `new test ${name}`; 85 | }, 86 | }; 87 | 88 | export default NewCommand; 89 | -------------------------------------------------------------------------------- /src/commands/starter/chrome-extension.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new TypeScript project 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['ce'], 10 | description: 'Creates a new Chrome extension', 11 | hidden: false, 12 | name: 'chrome-extension', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | filesystem, 17 | git, 18 | helper, 19 | meta, 20 | parameters, 21 | patching, 22 | print: { error, info, spin, success }, 23 | strings: { kebabCase }, 24 | system, 25 | } = toolbox; 26 | 27 | // Start timer 28 | const timer = system.startTimer(); 29 | 30 | // Info 31 | info('Create a new Chrome extension project'); 32 | 33 | // Check git 34 | if (!(await git.gitInstalled())) { 35 | return; 36 | } 37 | 38 | // Get name 39 | const name = await helper.getInput(parameters.first, { 40 | name: 'Project name', 41 | showError: true, 42 | }); 43 | if (!name) { 44 | return; 45 | } 46 | 47 | // Set project directory 48 | const projectDir = kebabCase(name); 49 | 50 | // Check if directory already exists 51 | if (filesystem.exists(projectDir)) { 52 | info(''); 53 | error(`There's already a folder named "${projectDir}" here.`); 54 | return undefined; 55 | } 56 | 57 | // Clone git repository 58 | const cloneSpinner = spin('Clone https://github.com/lenneTech/chrome-extension-angular-starter.git'); 59 | await system.run(`git clone https://github.com/lenneTech/chrome-extension-angular-starter.git ${projectDir}`); 60 | if (filesystem.isDirectory(`./${projectDir}`)) { 61 | filesystem.remove(`./${projectDir}/.git`); 62 | cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/chrome-extension-angular-starter.git'); 63 | } 64 | 65 | // Check directory 66 | if (!filesystem.isDirectory(`./${projectDir}`)) { 67 | error(`The directory "${projectDir}" could not be created.`); 68 | return undefined; 69 | } 70 | 71 | // Get author 72 | const author = await helper.getInput(parameters.second, { 73 | name: 'Author', 74 | showError: false, 75 | }); 76 | 77 | const prepareSpinner = spin('Prepare files'); 78 | 79 | // Set up initial props (to pass into templates) 80 | const nameKebab = kebabCase(name); 81 | 82 | // Patch readme 83 | await patching.replace(`./${projectDir}/README.md`, '# Starter for Chrome Extension via Angular', `# ${name}`); 84 | await patching.replace( 85 | `./${projectDir}/README.md`, 86 | 'This is the lenne.Tech starter for new Chrome Extension via Angular.', 87 | '', 88 | ); 89 | 90 | // Set package.json 91 | await patching.update(`./${projectDir}/package.json`, (config) => { 92 | config.author = author; 93 | config.bugs = { 94 | url: '', 95 | }; 96 | config.description = name; 97 | config.homepage = ''; 98 | config.name = nameKebab; 99 | config.repository = { 100 | type: 'git', 101 | url: '', 102 | }; 103 | config.version = '0.0.1'; 104 | return config; 105 | }); 106 | 107 | // Set package.json 108 | await patching.update(`./${projectDir}/package-lock.json`, (config) => { 109 | config.name = nameKebab; 110 | config.version = '0.0.1'; 111 | return config; 112 | }); 113 | 114 | // Set manifest.json 115 | await patching.update(`./${projectDir}/angular/src/manifest.json`, (config) => { 116 | config.name = name; 117 | config.short_name = name; 118 | config.description = ''; 119 | return config; 120 | }); 121 | 122 | // Patch app component 123 | await patching.replace(`./${projectDir}/angular/src/app/app.component.html`, 'Chrome Extension Starter', name); 124 | await patching.replace(`./${projectDir}/angular/src/app/app.component.html`, 'Chrome Extension starter', name); 125 | 126 | prepareSpinner.succeed('Files prepared'); 127 | 128 | // Init npm 129 | const installSpinner = spin('Install npm packages'); 130 | await system.run(`cd ${projectDir} && npm i`); 131 | installSpinner.succeed('NPM packages installed'); 132 | 133 | // Init git 134 | const initGitSpinner = spin('Initialize git'); 135 | await system.run( 136 | `cd ${projectDir} && git init && git add . && git commit -am "Init via lenne.Tech CLI ${meta.version()}"`, 137 | ); 138 | initGitSpinner.succeed('Git initialized'); 139 | 140 | // We're done, so show what to do next 141 | info(''); 142 | success(`Generated ${name} with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`); 143 | info(''); 144 | 145 | // For tests 146 | return `project ${name} created`; 147 | }, 148 | }; 149 | 150 | export default NewCommand; 151 | -------------------------------------------------------------------------------- /src/commands/starter/starter.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Starter commands 5 | */ 6 | module.exports = { 7 | alias: ['st'], 8 | description: 'Starter commands', 9 | hidden: true, 10 | name: 'starter', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('starter'); 13 | return 'starter'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/tools/crypt.ts: -------------------------------------------------------------------------------- 1 | import bcrypt = require('bcrypt'); 2 | import { GluegunCommand } from 'gluegun'; 3 | import { sha256 } from 'js-sha256'; 4 | 5 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 6 | 7 | /** 8 | * Open regex tools in browser 9 | */ 10 | const NewCommand: GluegunCommand = { 11 | alias: ['c', 'p', 'bcrypt', 'password'], 12 | description: 'Generate a password hash with bcrypt as in nest-server', 13 | hidden: false, 14 | name: 'crypt', 15 | run: async (toolbox: ExtendedGluegunToolbox) => { 16 | const { 17 | helper, 18 | parameters, 19 | print: { error, info }, 20 | } = toolbox; 21 | 22 | let password = await helper.getInput(parameters.first, { 23 | name: 'password to crypt', 24 | showError: false, 25 | }); 26 | 27 | if (!password) { 28 | error('No password provided'); 29 | return; 30 | } 31 | 32 | // Check if the password was transmitted encrypted 33 | // If not, the password is encrypted to enable future encrypted and unencrypted transmissions 34 | if (!/^[a-f0-9]{64}$/i.test(password)) { 35 | password = sha256(password); 36 | } 37 | 38 | // Hash password 39 | password = await bcrypt.hash(password, 10); 40 | info(password); 41 | 42 | if (!toolbox.parameters.options.fromGluegunMenu) { 43 | process.exit(); 44 | } 45 | 46 | // For tests 47 | return 'crypt'; 48 | }, 49 | }; 50 | 51 | export default NewCommand; 52 | -------------------------------------------------------------------------------- /src/commands/tools/jwt-read.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Parse a JWT and show the payload 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['jr'], 10 | description: 'Parse a JWT and show the payload', 11 | hidden: false, 12 | name: 'jwt-read', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | const { 15 | helper, 16 | parameters, 17 | print: { error, info }, 18 | } = toolbox; 19 | 20 | const jwt = await helper.getInput(parameters.first, { 21 | name: 'JWT to parse', 22 | showError: false, 23 | }); 24 | 25 | if (!jwt) { 26 | error('No JWT provided'); 27 | return; 28 | } 29 | 30 | // Hash password 31 | const data = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString()); 32 | info(data); 33 | if (data.iat) { 34 | info(`iat: ${new Date(data.iat * 1000)}`); 35 | } 36 | if (data.exp) { 37 | info(`exp: ${new Date(data.exp * 1000)}`); 38 | } 39 | 40 | if (!toolbox.parameters.options.fromGluegunMenu) { 41 | process.exit(); 42 | } 43 | 44 | // For tests 45 | return 'jwt-read'; 46 | }, 47 | }; 48 | 49 | export default NewCommand; 50 | -------------------------------------------------------------------------------- /src/commands/tools/regex.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | /** 4 | * Open regex tools in browser 5 | */ 6 | const NewCommand: GluegunCommand = { 7 | alias: ['r'], 8 | description: 'Open regex tools in browser', 9 | hidden: false, 10 | name: 'regex', 11 | run: async () => { 12 | 13 | const { default: open } = await import('open'); 14 | 15 | // Open link 16 | await open('https://regex101.com'); 17 | 18 | // For tests 19 | return 'open regex'; 20 | }, 21 | }; 22 | 23 | export default NewCommand; 24 | -------------------------------------------------------------------------------- /src/commands/tools/sha256.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { sha256 } from 'js-sha256'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Open regex tools in browser 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['h', 'hash'], 11 | description: 'Hash a string with sha256', 12 | hidden: false, 13 | name: 'sha256', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | const { 16 | helper, 17 | parameters, 18 | print: { info }, 19 | } = toolbox; 20 | 21 | const input = await helper.getInput(parameters.first, { 22 | name: 'string to hash', 23 | showError: false, 24 | }); 25 | const hashResult = await sha256(input); 26 | info(hashResult); 27 | 28 | if (!toolbox.parameters.options.fromGluegunMenu) { 29 | process.exit(); 30 | } 31 | 32 | // For tests 33 | return 'sha256'; 34 | }, 35 | }; 36 | 37 | export default NewCommand; 38 | -------------------------------------------------------------------------------- /src/commands/tools/tools.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * Tool commands 5 | */ 6 | module.exports = { 7 | alias: ['t'], 8 | description: 'Tools commands', 9 | hidden: true, 10 | name: 'tools', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('tools'); 13 | return 'tools'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/typescript/create.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | import { join } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Create a new TypeScript project 8 | */ 9 | const NewCommand: GluegunCommand = { 10 | alias: ['c', 'new', 'n'], 11 | description: 'Creates a new Typescript project', 12 | hidden: false, 13 | name: 'create', 14 | run: async (toolbox: ExtendedGluegunToolbox) => { 15 | // Retrieve the tools we need 16 | const { 17 | filesystem, 18 | git, 19 | helper, 20 | meta, 21 | npm, 22 | parameters, 23 | patching, 24 | print: { error, info, spin, success }, 25 | prompt: { confirm }, 26 | strings: { camelCase, kebabCase, pascalCase }, 27 | system, 28 | template, 29 | } = toolbox; 30 | 31 | // Start timer 32 | const timer = system.startTimer(); 33 | 34 | // Info 35 | info('Create a new TypeScript project'); 36 | 37 | // Check git 38 | if (!(await git.gitInstalled())) { 39 | return; 40 | } 41 | 42 | // Get name 43 | const name = await helper.getInput(parameters.first, { 44 | name: 'Project name', 45 | showError: true, 46 | }); 47 | if (!name) { 48 | return; 49 | } 50 | 51 | // Set project directory 52 | const projectDir = kebabCase(name); 53 | 54 | // Check if directory already exists 55 | if (filesystem.exists(projectDir)) { 56 | info(''); 57 | error(`There's already a folder named "${projectDir}" here.`); 58 | return undefined; 59 | } 60 | 61 | // Clone git repository 62 | const cloneSpinner = spin('Clone https://github.com/lenneTech/typescript-starter.git'); 63 | await system.run(`git clone https://github.com/lenneTech/typescript-starter.git ${projectDir}`); 64 | if (filesystem.isDirectory(`./${projectDir}`)) { 65 | filesystem.remove(`./${projectDir}/.git`); 66 | cloneSpinner.succeed('Repository cloned from https://github.com/lenneTech/typescript-starter.git'); 67 | } 68 | 69 | // Check directory 70 | if (!filesystem.isDirectory(`./${projectDir}`)) { 71 | error(`The directory "${projectDir}" could not be created.`); 72 | return undefined; 73 | } 74 | 75 | // Get author 76 | const author = await helper.getInput(parameters.second, { 77 | name: 'Author', 78 | showError: false, 79 | }); 80 | 81 | const prepareSpinner = spin('Prepare files'); 82 | 83 | // Set up initial props (to pass into templates) 84 | const nameCamel = camelCase(name); 85 | const nameKebab = kebabCase(name); 86 | const namePascal = pascalCase(name); 87 | 88 | // Set readme 89 | await template.generate({ 90 | props: { author, name, nameCamel, nameKebab, namePascal }, 91 | target: `./${projectDir}/README.md`, 92 | template: 'typescript-starter/README.md.ejs', 93 | }); 94 | 95 | // Set package.json 96 | await patching.update(`./${projectDir}/package.json`, (config) => { 97 | config.author = author; 98 | config.bugs = { 99 | url: '', 100 | }; 101 | config.description = name; 102 | config.homepage = ''; 103 | config.name = nameKebab; 104 | config.repository = { 105 | type: 'git', 106 | url: '', 107 | }; 108 | config.version = '0.0.1'; 109 | return config; 110 | }); 111 | 112 | // Set package.json 113 | await patching.update(`./${projectDir}/package-lock.json`, (config) => { 114 | config.name = nameKebab; 115 | config.version = '0.0.1'; 116 | return config; 117 | }); 118 | 119 | prepareSpinner.succeed('Files prepared'); 120 | 121 | // Install packages 122 | const update = await confirm('Do you want to install the latest versions of the included packages?', true); 123 | if (update) { 124 | // Update 125 | await npm.update({ cwd: join(filesystem.cwd(), projectDir), install: true, showError: true }); 126 | } else { 127 | // Init npm 128 | const installSpinner = spin('Install npm packages'); 129 | await system.run(`cd ${projectDir} && npm i`); 130 | installSpinner.succeed('NPM packages installed'); 131 | } 132 | 133 | // Init git 134 | const initGitSpinner = spin('Initialize git'); 135 | await system.run( 136 | `cd ${projectDir} && git init && git add . && git commit -am "Init via lenne.Tech CLI ${meta.version()}"`, 137 | ); 138 | initGitSpinner.succeed('Git initialized'); 139 | 140 | // We're done, so show what to do next 141 | info(''); 142 | success(`Generated ${name} with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`); 143 | info(''); 144 | 145 | // For tests 146 | return `project ${name} created`; 147 | }, 148 | }; 149 | 150 | export default NewCommand; 151 | -------------------------------------------------------------------------------- /src/commands/typescript/playground.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Create a new branch 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['pg'], 10 | description: 'Create a new typescript playground', 11 | hidden: false, 12 | name: 'playground', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | print: { error }, 17 | prompt: { ask }, 18 | typescript, 19 | } = toolbox; 20 | 21 | const choices = ['StackBlitz (online)', 'Web-Maker (download)', 'Simple typescript project']; 22 | 23 | // Select type 24 | const { type } = await ask({ 25 | choices: choices.slice(0), 26 | message: 'Select', 27 | name: 'type', 28 | type: 'select', 29 | }); 30 | 31 | switch (type) { 32 | case choices[0]: { 33 | await typescript.stackblitz(); 34 | break; 35 | } 36 | case choices[1]: { 37 | await typescript.webmaker(); 38 | break; 39 | } 40 | case choices[2]: { 41 | await typescript.create(); 42 | break; 43 | } 44 | default: { 45 | error(`No option selected!${type}`); 46 | return; 47 | } 48 | } 49 | 50 | // For tests 51 | return 'typescript'; 52 | }, 53 | }; 54 | 55 | export default NewCommand; 56 | -------------------------------------------------------------------------------- /src/commands/typescript/typescript.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox'; 2 | 3 | /** 4 | * TypeScript commands 5 | */ 6 | module.exports = { 7 | alias: ['ts'], 8 | description: 'Typescript commands', 9 | hidden: true, 10 | name: 'typescript', 11 | run: async (toolbox: ExtendedGluegunToolbox) => { 12 | await toolbox.helper.showMenu('typescript', { headline: 'TypeScript commands' }); 13 | return 'typescript'; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from '../interfaces/extended-gluegun-toolbox'; 4 | 5 | /** 6 | * Update @lenne.tech/cli 7 | */ 8 | const NewCommand: GluegunCommand = { 9 | alias: ['up'], 10 | description: 'Update @lenne.tech/cli', 11 | hidden: false, 12 | name: 'update', 13 | run: async (toolbox: ExtendedGluegunToolbox) => { 14 | // Retrieve the tools we need 15 | const { 16 | helper, 17 | runtime: { brand }, 18 | } = toolbox; 19 | 20 | // Update cli and show process 21 | await helper.updateCli({ showInfos: true }); 22 | 23 | // For tests 24 | return `updated ${brand}`; 25 | }, 26 | }; 27 | 28 | export default NewCommand; 29 | -------------------------------------------------------------------------------- /src/extensions/tools.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | 3 | import { ExtendedGluegunToolbox } from '../interfaces/extended-gluegun-toolbox'; 4 | 5 | const singleComment = Symbol('singleComment'); 6 | const multiComment = Symbol('multiComment'); 7 | 8 | const stripWithoutWhitespace = () => ''; 9 | const stripWithWhitespace = (string, start?, end?) => string.slice(start, end).replace(/\S/g, ' '); 10 | 11 | const isEscaped = (jsonString, quotePosition) => { 12 | let index = quotePosition - 1; 13 | let backslashCount = 0; 14 | 15 | while (jsonString[index] === '\\') { 16 | index -= 1; 17 | backslashCount += 1; 18 | } 19 | 20 | return Boolean(backslashCount % 2); 21 | }; 22 | 23 | export class Tools { 24 | /** 25 | * Constructor for integration of toolbox 26 | */ 27 | constructor(protected toolbox: ExtendedGluegunToolbox) {} 28 | 29 | /** 30 | * Strip and save JSON file 31 | */ 32 | stripAndSaveJsonFile(path: string) { 33 | const content = this.stripJsonComments(readFileSync(path, 'utf8')); 34 | writeFileSync(path, content); 35 | return content; 36 | } 37 | 38 | /** 39 | * Strip JSON comments from a string 40 | * Inspired by https://github.com/sindresorhus/strip-json-comments/blob/main/index.js 41 | */ 42 | stripJsonComments(jsonString, { trailingCommas = false, whitespace = true } = {}) { 43 | if (typeof jsonString !== 'string') { 44 | throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``); 45 | } 46 | 47 | const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace; 48 | 49 | let isInsideString = false; 50 | let isInsideComment: boolean | symbol = false; 51 | let offset = 0; 52 | let buffer = ''; 53 | let result = ''; 54 | let commaIndex = -1; 55 | 56 | for (let index = 0; index < jsonString.length; index++) { 57 | const currentCharacter = jsonString[index]; 58 | const nextCharacter = jsonString[index + 1]; 59 | 60 | if (!isInsideComment && currentCharacter === '"') { 61 | // Enter or exit string 62 | const escaped = isEscaped(jsonString, index); 63 | if (!escaped) { 64 | isInsideString = !isInsideString; 65 | } 66 | } 67 | 68 | if (isInsideString) { 69 | continue; 70 | } 71 | 72 | if (!isInsideComment && currentCharacter + nextCharacter === '//') { 73 | // Enter single-line comment 74 | buffer += jsonString.slice(offset, index); 75 | offset = index; 76 | isInsideComment = singleComment; 77 | index++; 78 | } else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') { 79 | // Exit single-line comment via \r\n 80 | index++; 81 | isInsideComment = false; 82 | buffer += strip(jsonString, offset, index); 83 | offset = index; 84 | continue; 85 | } else if (isInsideComment === singleComment && currentCharacter === '\n') { 86 | // Exit single-line comment via \n 87 | isInsideComment = false; 88 | buffer += strip(jsonString, offset, index); 89 | offset = index; 90 | } else if (!isInsideComment && currentCharacter + nextCharacter === '/*') { 91 | // Enter multiline comment 92 | buffer += jsonString.slice(offset, index); 93 | offset = index; 94 | isInsideComment = multiComment; 95 | index++; 96 | continue; 97 | } else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') { 98 | // Exit multiline comment 99 | index++; 100 | isInsideComment = false; 101 | buffer += strip(jsonString, offset, index + 1); 102 | offset = index + 1; 103 | continue; 104 | } else if (trailingCommas && !isInsideComment) { 105 | if (commaIndex !== -1) { 106 | if (currentCharacter === '}' || currentCharacter === ']') { 107 | // Strip trailing comma 108 | buffer += jsonString.slice(offset, index); 109 | result += strip(buffer, 0, 1) + buffer.slice(1); 110 | buffer = ''; 111 | offset = index; 112 | commaIndex = -1; 113 | } else if ( 114 | currentCharacter !== ' ' 115 | && currentCharacter !== '\t' 116 | && currentCharacter !== '\r' 117 | && currentCharacter !== '\n' 118 | ) { 119 | // Hit non-whitespace following a comma; comma is not trailing 120 | buffer += jsonString.slice(offset, index); 121 | offset = index; 122 | commaIndex = -1; 123 | } 124 | } else if (currentCharacter === ',') { 125 | // Flush buffer prior to this point, and save new comma index 126 | result += buffer + jsonString.slice(offset, index); 127 | buffer = ''; 128 | offset = index; 129 | commaIndex = index; 130 | } 131 | } 132 | } 133 | 134 | return result + buffer + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset)); 135 | } 136 | } 137 | 138 | /** 139 | * Extend toolbox 140 | */ 141 | export default (toolbox: ExtendedGluegunToolbox) => { 142 | toolbox.tools = new Tools(toolbox); 143 | }; 144 | -------------------------------------------------------------------------------- /src/extensions/typescript.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { join } from 'path'; 3 | 4 | import { ExtendedGluegunToolbox } from '../interfaces/extended-gluegun-toolbox'; 5 | 6 | /** 7 | * Common helper functions 8 | */ 9 | export class Typescript { 10 | /** 11 | * Constructor for integration of toolbox 12 | */ 13 | constructor(protected toolbox: ExtendedGluegunToolbox) {} 14 | 15 | /** 16 | * Create a simple typescript project 17 | */ 18 | public async create() { 19 | // Toolbox features 20 | const { 21 | filesystem: { cwd, existsAsync }, 22 | helper, 23 | npm, 24 | print: { error, info, spin, success }, 25 | system: { run, startTimer, which }, 26 | } = this.toolbox; 27 | 28 | // Get project name 29 | const name = await helper.getInput(null, { 30 | name: 'project name', 31 | showError: true, 32 | }); 33 | if (!name) { 34 | return; 35 | } 36 | 37 | // Check dir 38 | const dir = join(cwd(), name); 39 | if (await existsAsync(dir)) { 40 | error(`Diretory ${dir} exists!`); 41 | } 42 | 43 | // Start timer 44 | const timer = startTimer(); 45 | 46 | // Init 47 | const cloneSpin = spin(`Init project ${name}`); 48 | 49 | // Init gts 50 | fs.mkdirSync(dir); 51 | await run(`cd ${dir} && npm init -y && npx gts init && npm install -D ts-node`); 52 | 53 | // Prepare package.json 54 | const { data, path } = await npm.getPackageJson({ 55 | cwd: dir, 56 | showError: true, 57 | }); 58 | if (!path) { 59 | return; 60 | } 61 | data.scripts.start = 'npx ts-node src/index.ts'; 62 | data.main = 'build/index.js'; 63 | if (!(await npm.setPackageJson(data, { cwd: dir, showError: true }))) { 64 | return; 65 | } 66 | 67 | // Overwrite index.ts 68 | const pathOfIndex = join(dir, 'src', 'index.ts'); 69 | fs.unlinkSync(pathOfIndex); 70 | fs.writeFileSync(pathOfIndex, '// Write your code here\nconsole.log(\'hello world!\');'); 71 | 72 | // Init git 73 | if (which('git')) { 74 | await run('git init'); 75 | } 76 | 77 | cloneSpin.succeed(); 78 | 79 | // Success info 80 | success(`Project ${name} was created in ${helper.msToMinutesAndSeconds(timer())}.`); 81 | info(''); 82 | } 83 | 84 | /** 85 | * Open stackblitz 86 | */ 87 | public async stackblitz() { 88 | const { default: open } = await import('open'); 89 | return open('https://stackblitz.com/fork/typescript'); 90 | } 91 | 92 | /** 93 | * Download and install Web-Maker 94 | */ 95 | public async webmaker() { 96 | // Toolbox features 97 | const { 98 | filesystem: { cwd, existsAsync }, 99 | git, 100 | helper, 101 | npm, 102 | print: { error, spin }, 103 | system: { run }, 104 | } = this.toolbox; 105 | 106 | // Check git 107 | if (!(await git.gitInstalled())) { 108 | return; 109 | } 110 | 111 | // Get project name 112 | const name = await helper.getInput(null, { 113 | name: 'project name', 114 | showError: true, 115 | }); 116 | if (!name) { 117 | return; 118 | } 119 | 120 | // Check dir 121 | const dir = join(cwd(), name); 122 | if (await existsAsync(dir)) { 123 | error(`Diretory ${dir} exists!`); 124 | } 125 | 126 | // Clone 127 | const repository = 'https://github.com/chinchang/web-maker.git'; 128 | const cloneSpin = spin(`Cloning web-maker: ${repository}`); 129 | await run(`git clone ${repository} ${dir}`); 130 | cloneSpin.succeed(); 131 | 132 | // Install packages 133 | if (await npm.install({ cwd: dir, showError: true })) { 134 | return; 135 | } 136 | 137 | // Start 138 | await run(`cd ${dir} && npm start`); 139 | } 140 | } 141 | 142 | /** 143 | * Extend toolbox 144 | */ 145 | export default (toolbox: ExtendedGluegunToolbox) => { 146 | toolbox.typescript = new Typescript(toolbox); 147 | }; 148 | -------------------------------------------------------------------------------- /src/interfaces/ServerProps.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server properties for models and inputs 3 | */ 4 | export interface ServerProps { 5 | declare?: boolean; 6 | enumRef: string; 7 | isArray: boolean; 8 | name: string; 9 | nullable: boolean; 10 | reference: string; 11 | schema: string; 12 | type: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/extended-gluegun-command.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun'; 2 | 3 | import { ExtendedGluegunToolbox } from './extended-gluegun-toolbox'; 4 | 5 | export interface ExtendedGluegunCommand extends GluegunCommand { 6 | run: ( 7 | toolbox: ExtendedGluegunToolbox, 8 | options?: { 9 | currentItem?: string; 10 | objectsToAdd?: { object: string; property: string }[]; 11 | preventExitProcess?: boolean; 12 | referencesToAdd?: { property: string; reference: string }[]; 13 | } 14 | ) => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/extended-gluegun-toolbox.ts: -------------------------------------------------------------------------------- 1 | import { IHelperExtendedGluegunToolbox } from '@lenne.tech/cli-plugin-helper'; 2 | 3 | import { Git } from '../extensions/git'; 4 | import { Server } from '../extensions/server'; 5 | import { Tools } from '../extensions/tools'; 6 | import { Typescript } from '../extensions/typescript'; 7 | 8 | /** 9 | * Extended GluegunToolbox 10 | */ 11 | export interface ExtendedGluegunToolbox extends IHelperExtendedGluegunToolbox { 12 | git: Git; 13 | server: Server; 14 | tools: Tools; 15 | typescript: Typescript; 16 | } 17 | -------------------------------------------------------------------------------- /src/lt.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration of CLI 3 | */ 4 | module.exports = { 5 | defaults: { 6 | checkForUpdate: true, 7 | }, 8 | name: 'lt', 9 | }; 10 | -------------------------------------------------------------------------------- /src/templates/deployment/.github/workflows/pre-release.yml.ejs: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | 8 | env: 9 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 10 | 11 | jobs: 12 | deploy: 13 | runs-on: [self-hosted, docker-live] 14 | env: 15 | STACK_NAME: <%= props.nameCamel %> 16 | APP_URL: test.<%= props.url %> 17 | CI_REGISTRY_IMAGE: localhost:5000/<%= props.nameCamel %> 18 | FILE_NAME: docker-compose.test.yml 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js 18 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 18 25 | - name: Install 26 | run: npm run init 27 | - name: Build app 28 | run: npm run build:test 29 | - name: Build docker 30 | run: STACK_NAME=${{env.STACK_NAME}} APP_URL=${{env.APP_URL}} IMAGE_TAG=test CI_REGISTRY_IMAGE=${{env.CI_REGISTRY_IMAGE}} sh build-push.sh 31 | - name: Deploy 32 | run: FILE_NAME=${{env.FILE_NAME}} STACK_NAME=${{env.STACK_NAME}} APP_URL=${{env.APP_URL}} IMAGE_TAG=test CI_REGISTRY_IMAGE=${{env.CI_REGISTRY_IMAGE}} sh deploy.sh 33 | - name: Deploy notification 34 | if: always() 35 | uses: adamkdean/simple-slack-notify@master 36 | with: 37 | channel: "#deployments" 38 | status: ${{ job.status }} 39 | success_text: "Version (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> wurde erfolgreich auf *Test* deployed." 40 | failure_text: "Testversion (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> ist fehlgeschlagen." 41 | cancelled_text: "Testversion (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> wurde abgebrochen." 42 | -------------------------------------------------------------------------------- /src/templates/deployment/.github/workflows/release.yml.ejs: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | 8 | env: 9 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 10 | 11 | jobs: 12 | deploy: 13 | runs-on: [self-hosted, docker-live-swaktiv] 14 | env: 15 | STACK_NAME: <%= props.nameCamel %> 16 | APP_URL: <%= props.url %> 17 | CI_REGISTRY_IMAGE: localhost:5000/<%= props.nameCamel %> 18 | FILE_NAME: docker-compose.prod.yml 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js 18 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 18 25 | - name: Install 26 | run: npm run init 27 | - name: Build app 28 | run: npm run build 29 | - name: Build docker 30 | run: STACK_NAME=${{env.STACK_NAME}} APP_URL=${{env.APP_URL}} IMAGE_TAG=latest CI_REGISTRY_IMAGE=${{env.CI_REGISTRY_IMAGE}} sh build-push.sh 31 | - name: Deploy 32 | run: FILE_NAME=${{env.FILE_NAME}} STACK_NAME=${{env.STACK_NAME}} APP_URL=${{env.APP_URL}} IMAGE_TAG=latest CI_REGISTRY_IMAGE=${{env.CI_REGISTRY_IMAGE}} sh deploy.sh 33 | - name: Deploy notification 34 | if: always() 35 | uses: adamkdean/simple-slack-notify@master 36 | with: 37 | channel: "#deployments" 38 | status: ${{ job.status }} 39 | success_text: "Version (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> wurde erfolgreich auf *Live* deployed." 40 | failure_text: "Release (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> ist fehlgeschlagen." 41 | cancelled_text: "Release (#${{ github.event.release.tag_name }}) von <%= props.nameCamel %> wurde abgebrochen." 42 | -------------------------------------------------------------------------------- /src/templates/deployment/.gitlab-ci.yml.ejs: -------------------------------------------------------------------------------- 1 | image: node:18-alpine 2 | 3 | stages: 4 | - install_dependencies 5 | # - version_number 6 | - build 7 | - package 8 | - deploy 9 | 10 | variables: 11 | APP_URL_PROD: <%= props.url %> 12 | APP_URL_TEST: test.<%= props.url %> 13 | STACK_NAME: $CI_PROJECT_NAME 14 | FILE_NAME_PROD: docker-compose.prod.yml 15 | FILE_NAME_TEST: docker-compose.test.yml 16 | CI_NAME: 'gitlab' 17 | CI_EMAIL: 'gitlab-ci@example.com' 18 | 19 | install_dependencies: 20 | stage: install_dependencies 21 | cache: 22 | key: $CI_PROJECT_DIR 23 | paths: 24 | - node_modules/ 25 | policy: push 26 | script: 27 | - npm ci 28 | only: 29 | refs: 30 | - dev 31 | - test 32 | - release 33 | - preview 34 | - main 35 | changes: 36 | - package-lock.json 37 | 38 | build_review: 39 | stage: build 40 | cache: 41 | key: $CI_PROJECT_DIR 42 | paths: 43 | - node_modules/ 44 | policy: pull 45 | artifacts: 46 | paths: 47 | - projects/api/dist/ 48 | - projects/app/dist/ 49 | expire_in: 5 minutes 50 | script: 51 | - npm run init 52 | - npm run build 53 | rules: 54 | - if: $CI_MERGE_REQUEST_ID && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "test" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "release" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "preview" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "main" 55 | 56 | build:prod: 57 | stage: build 58 | cache: 59 | key: $CI_PROJECT_DIR 60 | paths: 61 | - node_modules/ 62 | policy: pull 63 | artifacts: 64 | paths: 65 | - projects/api/dist/ 66 | - projects/app/dist/ 67 | expire_in: 5 minutes 68 | script: 69 | - npm run init 70 | - npm run build 71 | only: 72 | - main 73 | 74 | build:test: 75 | stage: build 76 | image: tarampampam/node:16-alpine 77 | cache: 78 | key: $CI_PROJECT_DIR 79 | paths: 80 | - node_modules/ 81 | policy: pull 82 | artifacts: 83 | paths: 84 | - projects/api/dist/ 85 | - projects/app/dist/ 86 | expire_in: 5 minutes 87 | script: 88 | - npm run init 89 | - npm run build:test 90 | only: 91 | - test 92 | 93 | build:dev: 94 | stage: build 95 | cache: 96 | key: $CI_PROJECT_DIR 97 | paths: 98 | - node_modules/ 99 | policy: pull 100 | artifacts: 101 | paths: 102 | - projects/api/dist/ 103 | - projects/app/dist/ 104 | expire_in: 5 minutes 105 | script: 106 | - npm run init 107 | - npm run build:dev 108 | only: 109 | - dev 110 | 111 | #version_number: 112 | # stage: version_number 113 | # image: tarampampam/node:alpine 114 | # script: 115 | # - git config --global user.email $CI_EMAIL 116 | # - git config --global user.name $CI_NAME 117 | # - git config http.sslVerify "false" 118 | # - npm install 119 | # - git config receive.advertisePushOptions true 120 | # - git checkout -B "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA" 121 | # - npm run release 122 | # - git push -o ci.skip --no-verify https://${CI_USER}:${CI_ACCESS_TOKEN}@gitlab.lenne.tech/products/akademie/master-minds.git --follow-tags test:test 123 | # - git fetch && git checkout dev 124 | # - git merge test 125 | # - git push -o ci.skip --no-verify https://${CI_USER}:${CI_ACCESS_TOKEN}@gitlab.lenne.tech/products/akademie/master-minds.git --follow-tags dev:dev 126 | # only: 127 | # - test 128 | 129 | docker_build_push_test: 130 | stage: package 131 | image: tiangolo/docker-with-compose 132 | services: 133 | - docker:dind 134 | before_script: 135 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 136 | script: 137 | - FILE_NAME=$FILE_NAME_TEST STACK_NAME=$STACK_NAME APP_URL=$APP_URL_TEST IMAGE_TAG=test CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE sh ./scripts/build-push.sh 138 | only: 139 | - test 140 | 141 | deploy_test: 142 | stage: deploy 143 | image: tiangolo/docker-with-compose 144 | tags: 145 | - <%= props.testRunner %> 146 | before_script: 147 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 148 | script: 149 | - FILE_NAME=$FILE_NAME_TEST STACK_NAME=$STACK_NAME APP_URL=$APP_URL_TEST IMAGE_TAG=test CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE sh ./scripts/deploy.sh 150 | environment: 151 | name: test 152 | url: https://$APP_URL_TEST 153 | only: 154 | - test 155 | 156 | docker_build_push_prod: 157 | stage: package 158 | image: tiangolo/docker-with-compose 159 | dependencies: 160 | - build:prod 161 | before_script: 162 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 163 | script: 164 | - FILE_NAME=$FILE_NAME_PROD STACK_NAME=$STACK_NAME APP_URL=$APP_URL_PROD IMAGE_TAG=production CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE sh ./scripts/build-push.sh 165 | only: 166 | - main 167 | 168 | deploy_prod: 169 | stage: deploy 170 | image: tiangolo/docker-with-compose 171 | tags: 172 | - <%= props.prodRunner %> 173 | before_script: 174 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 175 | script: 176 | - FILE_NAME=$FILE_NAME_PROD STACK_NAME=$STACK_NAME APP_URL=$APP_URL_PROD IMAGE_TAG=production CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE sh ./scripts/deploy.sh 177 | environment: 178 | name: production 179 | url: https://$APP_URL_PROD 180 | only: 181 | - main 182 | -------------------------------------------------------------------------------- /src/templates/deployment/Dockerfile.app.ejs: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:16 2 | 3 | RUN mkdir -p /var/www 4 | 5 | RUN apk --no-cache add curl 6 | 7 | COPY ./projects/app/dist ./var/www/dist 8 | 9 | HEALTHCHECK CMD curl --fail http://localhost:4000/ || exit 1 10 | 11 | WORKDIR /var/www 12 | 13 | EXPOSE 4000 14 | -------------------------------------------------------------------------------- /src/templates/deployment/Dockerfile.ejs: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:16.4.2 2 | 3 | RUN mkdir -p /var/www/api 4 | 5 | RUN apk --no-cache add curl 6 | 7 | ADD ./projects/api/package.json /var/www/api/package.json 8 | ADD ./projects/api/package-lock.json /var/www/api/package-lock.json 9 | 10 | COPY ./projects/api/dist ./var/www/api 11 | 12 | RUN cd /var/www/api && npm install && npm cache clean --force 13 | 14 | HEALTHCHECK --interval=60s --retries=5 CMD curl --fail http://localhost:3000/meta/ || exit 1 15 | 16 | WORKDIR /var/www/api 17 | 18 | EXPOSE 3000 19 | -------------------------------------------------------------------------------- /src/templates/deployment/docker-compose.dev.yml.ejs: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | networks: 4 | traefik-public: 5 | external: true 6 | overlay_mongo: 7 | external: true 8 | 9 | services: 10 | api: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | image: ${CI_REGISTRY_IMAGE?Variable not set}/api:${IMAGE_TAG?Variable not set} 15 | restart: unless-stopped 16 | container_name: swaktiv-api-${IMAGE_TAG?Variable not set} 17 | networks: 18 | - traefik-public 19 | - overlay_mongo 20 | deploy: 21 | placement: 22 | constraints: 23 | - node.labels.traefik-public.traefik-public-certificates == true 24 | update_config: 25 | order: start-first 26 | failure_action: rollback 27 | delay: 10s 28 | rollback_config: 29 | parallelism: 0 30 | order: stop-first 31 | restart_policy: 32 | condition: any 33 | delay: 5s 34 | max_attempts: 3 35 | window: 120s 36 | labels: 37 | - traefik.enable=true 38 | - traefik.docker.network=traefik-public 39 | - traefik.constraint-label=traefik-public 40 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.rule=Host(`api.${APP_URL?Variable not set}`) 41 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.entrypoints=http 42 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.middlewares=https-redirect 43 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.rule=Host(`api.${APP_URL?Variable not set}`) 44 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.entrypoints=https 45 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls=true 46 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls.certresolver=le 47 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api.loadbalancer.server.port=3000 48 | entrypoint: ["/bin/sh", "-c"] 49 | command: 50 | - | 51 | npm run migrate:dev:up 52 | NODE_ENV=dev node ./src/main.js 53 | 54 | app: 55 | build: 56 | context: . 57 | dockerfile: Dockerfile.app 58 | image: ${CI_REGISTRY_IMAGE?Variable not set}/app-ssr:${IMAGE_TAG?Variable not set} 59 | restart: unless-stopped 60 | container_name: swaktiv-app-${IMAGE_TAG?Variable not set} 61 | entrypoint: ["/bin/sh", "-c"] 62 | networks: 63 | - traefik-public 64 | deploy: 65 | placement: 66 | constraints: 67 | - node.labels.traefik-public.traefik-public-certificates == true 68 | update_config: 69 | order: start-first 70 | failure_action: rollback 71 | delay: 10s 72 | rollback_config: 73 | parallelism: 0 74 | order: stop-first 75 | restart_policy: 76 | condition: any 77 | delay: 5s 78 | max_attempts: 3 79 | window: 120s 80 | labels: 81 | - traefik.enable=true 82 | - traefik.docker.network=traefik-public 83 | - traefik.constraint-label=traefik-public 84 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 85 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.entrypoints=http 86 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.middlewares=https-redirect 87 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 88 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.entrypoints=https 89 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls=true 90 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls.certresolver=le 91 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.regex=^https?://www.${APP_URL}/(.*) 92 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.replacement=https://${APP_URL}/$${1} 93 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.permanent=true 94 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app.loadbalancer.server.port=4000 95 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.middlewares=${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect 96 | 97 | command: 98 | - | 99 | NODE_ENV=dev node dist/app/server/main.js 100 | -------------------------------------------------------------------------------- /src/templates/deployment/docker-compose.prod.yml.ejs: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | networks: 4 | traefik-public: 5 | external: true 6 | overlay_mongo: 7 | external: true 8 | 9 | services: 10 | api: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | image: ${CI_REGISTRY_IMAGE?Variable not set}/api:${IMAGE_TAG?Variable not set} 15 | restart: unless-stopped 16 | container_name: <%= props.nameCamel %>-api-${IMAGE_TAG?Variable not set} 17 | networks: 18 | - traefik-public 19 | - overlay_mongo 20 | deploy: 21 | update_config: 22 | order: start-first 23 | failure_action: rollback 24 | delay: 10s 25 | rollback_config: 26 | parallelism: 0 27 | order: stop-first 28 | restart_policy: 29 | condition: any 30 | delay: 5s 31 | max_attempts: 3 32 | window: 120s 33 | labels: 34 | - traefik.enable=true 35 | - traefik.docker.network=traefik-public 36 | - traefik.constraint-label=traefik-public 37 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.rule=Host(`api.${APP_URL?Variable not set}`) 38 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.entrypoints=http 39 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.middlewares=https-redirect 40 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.rule=Host(`api.${APP_URL?Variable not set}`) 41 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.entrypoints=https 42 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls=true 43 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls.certresolver=le 44 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api.loadbalancer.server.port=3000 45 | entrypoint: ["/bin/sh", "-c"] 46 | command: 47 | - | 48 | npm run migrate:prod:up 49 | NODE_ENV=production node ./src/main.js 50 | 51 | app: 52 | build: 53 | context: . 54 | dockerfile: Dockerfile.app 55 | image: ${CI_REGISTRY_IMAGE?Variable not set}/app-ssr:${IMAGE_TAG?Variable not set} 56 | restart: unless-stopped 57 | container_name: <%= props.nameCamel %>-app-${IMAGE_TAG?Variable not set} 58 | entrypoint: ["/bin/sh", "-c"] 59 | networks: 60 | - traefik-public 61 | deploy: 62 | update_config: 63 | order: start-first 64 | failure_action: rollback 65 | delay: 10s 66 | rollback_config: 67 | parallelism: 0 68 | order: stop-first 69 | restart_policy: 70 | condition: any 71 | delay: 5s 72 | max_attempts: 3 73 | window: 120s 74 | labels: 75 | - traefik.enable=true 76 | - traefik.docker.network=traefik-public 77 | - traefik.constraint-label=traefik-public 78 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 79 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.entrypoints=http 80 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.middlewares=https-redirect 81 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 82 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.entrypoints=https 83 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls=true 84 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls.certresolver=le 85 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.regex=^https?://www.${APP_URL}/(.*) 86 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.replacement=https://${APP_URL}/$${1} 87 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.permanent=true 88 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app.loadbalancer.server.port=4000 89 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.middlewares=${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect 90 | command: 91 | - | 92 | NODE_ENV=production node dist/app/server/main.js 93 | -------------------------------------------------------------------------------- /src/templates/deployment/docker-compose.test.yml.ejs: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | networks: 4 | traefik-public: 5 | external: true 6 | overlay_mongo: 7 | external: true 8 | 9 | services: 10 | api: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | image: ${CI_REGISTRY_IMAGE?Variable not set}/api:${IMAGE_TAG?Variable not set} 15 | restart: unless-stopped 16 | container_name: <%= props.nameCamel %>-api-${IMAGE_TAG?Variable not set} 17 | networks: 18 | - traefik-public 19 | - overlay_mongo 20 | deploy: 21 | placement: 22 | constraints: 23 | - node.labels.traefik-public.traefik-public-certificates == true 24 | update_config: 25 | order: start-first 26 | failure_action: rollback 27 | delay: 10s 28 | rollback_config: 29 | parallelism: 0 30 | order: stop-first 31 | restart_policy: 32 | condition: any 33 | delay: 5s 34 | max_attempts: 3 35 | window: 120s 36 | labels: 37 | - traefik.enable=true 38 | - traefik.docker.network=traefik-public 39 | - traefik.constraint-label=traefik-public 40 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.rule=Host(`api.${APP_URL?Variable not set}`) 41 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.entrypoints=http 42 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-http.middlewares=https-redirect 43 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.rule=Host(`api.${APP_URL?Variable not set}`) 44 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.entrypoints=https 45 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls=true 46 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api-https.tls.certresolver=le 47 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-api.loadbalancer.server.port=3000 48 | entrypoint: ["/bin/sh", "-c"] 49 | command: 50 | - | 51 | npm run migrate:test:up 52 | NODE_ENV=test node ./src/main.js 53 | 54 | app: 55 | build: 56 | context: . 57 | dockerfile: Dockerfile.app 58 | image: ${CI_REGISTRY_IMAGE?Variable not set}/app-ssr:${IMAGE_TAG?Variable not set} 59 | restart: unless-stopped 60 | container_name: <%= props.nameCamel %>-app-${IMAGE_TAG?Variable not set} 61 | entrypoint: ["/bin/sh", "-c"] 62 | networks: 63 | - traefik-public 64 | deploy: 65 | placement: 66 | constraints: 67 | - node.labels.traefik-public.traefik-public-certificates == true 68 | update_config: 69 | order: start-first 70 | failure_action: rollback 71 | delay: 10s 72 | rollback_config: 73 | parallelism: 0 74 | order: stop-first 75 | restart_policy: 76 | condition: any 77 | delay: 5s 78 | max_attempts: 3 79 | window: 120s 80 | labels: 81 | - traefik.enable=true 82 | - traefik.docker.network=traefik-public 83 | - traefik.constraint-label=traefik-public 84 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 85 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.entrypoints=http 86 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-http.middlewares=https-redirect 87 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.rule=Host(`${APP_URL?Variable not set}`, `www.${APP_URL?Variable not set}`) 88 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.entrypoints=https 89 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls=true 90 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.tls.certresolver=le 91 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.regex=^https?://www.${APP_URL}/(.*) 92 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.replacement=https://${APP_URL}/$${1} 93 | - traefik.http.middlewares.${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect.redirectregex.permanent=true 94 | - traefik.http.services.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app.loadbalancer.server.port=4000 95 | - traefik.http.routers.${STACK_NAME?Variable not set}-${IMAGE_TAG}-app-https.middlewares=${STACK_NAME?Variable not set}-${IMAGE_TAG}-redirect 96 | command: 97 | - | 98 | NODE_ENV=test node dist/app/server/main.js 99 | -------------------------------------------------------------------------------- /src/templates/deployment/scripts/build-push.sh.ejs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | # Exit in case of error 4 | set -e 5 | 6 | STACK_NAME=${STACK_NAME?Variable not set} \ 7 | APP_URL=${APP_URL?Variable not set} \ 8 | IMAGE_TAG=${IMAGE_TAG?Variable not set} \ 9 | CI_REGISTRY_IMAGE=${CI_REGISTRY_IMAGE?Variable not set} \ 10 | docker compose \ 11 | -f ${FILE_NAME?Variable not set} \ 12 | build 13 | 14 | STACK_NAME=${STACK_NAME?Variable not set} \ 15 | APP_URL=${APP_URL?Variable not set} \ 16 | IMAGE_TAG=${IMAGE_TAG?Variable not set} \ 17 | CI_REGISTRY_IMAGE=${CI_REGISTRY_IMAGE?Variable not set} \ 18 | docker compose \ 19 | -f ${FILE_NAME?Variable not set} \ 20 | push 21 | -------------------------------------------------------------------------------- /src/templates/deployment/scripts/deploy.sh.ejs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | 3 | # Exit in case of error 4 | set -e 5 | 6 | docker stack deploy -c ${FILE_NAME?Variable not set} --with-registry-auth ${STACK_NAME?Variable not set}-${IMAGE_TAG} 7 | 8 | -------------------------------------------------------------------------------- /src/templates/model.ts.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: '<%= props.name %>' 3 | } 4 | -------------------------------------------------------------------------------- /src/templates/monorepro/README.md.ejs: -------------------------------------------------------------------------------- 1 | # <%= props.name %> 2 | 3 | This is a [lerna](https://lerna.js.org/) [Monorepro](https://monorepo.tools) for App ([Angular](https://angular.io)) and API ([NestJS](https://nestjs.com)). 4 | Build with [NgBaseStarter](https://github.com/lenneTech/ng-base-starter) and [Starter for Nest Server](https://github.com/lenneTech/nest-server-starter) 5 | via [CLI](https://github.com/lenneTech/cli). 6 | 7 | If you are not yet familiar with Angular or NestJS, we recommend you to have a look at the following sections of our 8 | academy [lenne.Learning](https://lennelearning.de): 9 | - [Angular](https://lennelearning.de/lernpfad/angular) 10 | - [NestJS](https://lennelearning.de/lernpfad/nestjs) 11 | 12 | ## Requirements 13 | 14 | - [Node.js incl. npm](https://nodejs.org): 15 | the runtime environment for your server 16 | 17 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git): 18 | the version control system for your source code 19 | 20 | - [MongoDB](https://docs.mongodb.com/manual/installation/#mongodb-community-edition-installation-tutorials) 21 | (or any other database compatible with [MikroORM](https://mikro-orm.io)): 22 | the database for your objects 23 | 24 | Installation on MacOS: 25 | ``` 26 | /bin/bash -c "$(curl - fsSL https: //raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 27 | brew tap mongodb/brew 28 | brew update 29 | brew install mongodb-community@6.O 30 | brew services start mongodb 31 | brew install node 32 | npm install -g n 33 | ``` 34 | 35 | To check if all requirements are met, we can display the versions of the respective software 36 | (the version may differ from your versions in the following example): 37 | ``` 38 | mongod --version 39 | db version v6.0.1 40 | 41 | node -v 42 | v16.17.0 43 | 44 | npm -v 45 | 8.15.0 46 | ``` 47 | 48 | ## Initialization of a local instance 49 | 50 | Clone this repository and install packages: 51 | ``` 52 | git clone <%= props.repository %> 53 | cd <%= props.nameKebab %> 54 | npm run init 55 | ``` 56 | 57 | ## Testing 58 | 59 | Check if the requirements are met by running the tests (in the root directory): 60 | ``` 61 | npm test 62 | ``` 63 | 64 | ## Start 65 | 66 | Both the app and the API are started with the following command (in the root directory): 67 | ``` 68 | npm start 69 | ``` 70 | 71 | The following URLs are then accessible: 72 | 73 | - App: http://localhost:4200 74 | - Documentation of the API: http://localhost:3000 75 | - Playground for using the API: http://localhost:3000/graphql 76 | 77 | If a URL is not reachable, it may be due to an error in the source code or because another process is already 78 | occupying the corresponding port (3000 / 4200). In this case you can either stop the other process or set another 79 | port in the configuration of the app or server. 80 | 81 | ## Architecture 82 | 83 | The architecture of the projects is based on the following structures: 84 | - Both the app and the API can be found with their respective dependencies in the `projects` directory 85 | - Both technologies are still compatible with their own CLI ([Angular CLI](angular cli) / [NestJS CLI](https://docs.nestjs.com/cli/overview)) 86 | - The [lt CLI](https://github.com/lenneTech/cli) also offers a few benefits regarding the extensions [NgBaseStarter](https://github.com/lenneTech/ng-base-starter) and [Starter for Nest Server](https://github.com/lenneTech/nest-server-starter) 87 | - To organize the monorepro [lerna](https://lerna.js.org/) is used 88 | 89 | ## History 90 | - Initialized via `lt angular create` 91 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/inputs/template-create.input.ts.ejs: -------------------------------------------------------------------------------- 1 | import { Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; 2 | import { InputType } from '@nestjs/graphql'; 3 | 4 | import { <%= props.namePascal %>Input } from './<%= props.nameKebab %>.input'; 5 | 6 | 7 | /** 8 | * <%= props.namePascal %> create input 9 | */ 10 | @Restricted(RoleEnum.ADMIN) 11 | @InputType({ description: 'Input data to create a new <%= props.namePascal %>' }) 12 | export class <%= props.namePascal %>CreateInput extends <%= props.namePascal %>Input { 13 | 14 | // =================================================================================================================== 15 | // Properties 16 | // =================================================================================================================== 17 | <%- props.props %> 18 | } 19 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/inputs/template.input.ts.ejs: -------------------------------------------------------------------------------- 1 | import { CoreInput, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; 2 | import { InputType } from '@nestjs/graphql'; 3 | 4 | /** 5 | * <%= props.namePascal %> input 6 | */ 7 | @Restricted(RoleEnum.ADMIN) 8 | @InputType({ description: 'Input data to update an existing <%= props.namePascal %>' }) 9 | export class <%= props.namePascal %>Input extends CoreInput { 10 | 11 | // =================================================================================================================== 12 | // Properties 13 | // =================================================================================================================== 14 | <%- props.props %> 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs: -------------------------------------------------------------------------------- 1 | import { UnifiedField } from '@lenne.tech/nest-server'; 2 | <% if (props.isGql) { %> 3 | import { ObjectType } from '@nestjs/graphql'; 4 | <% } %> 5 | 6 | import { <%= props.namePascal %> } from '../<%= props.nameKebab %>.model'; 7 | <% if (props.isGql) { %> 8 | @ObjectType({ description: 'Result of find and count <%= props.namePascal %>s' }) 9 | <% } %> 10 | export class FindAndCount<%= props.namePascal %>sResult { 11 | 12 | @UnifiedField({ 13 | type: () => <%= props.namePascal %>, 14 | description: 'Found <%= props.namePascal %>s', 15 | }) 16 | items: <%= props.namePascal %>[]; 17 | 18 | @UnifiedField({ 19 | description: 'Total count (skip/offset and limit/take are ignored in the count)', 20 | }) 21 | totalCount: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/template.controller.ts.ejs: -------------------------------------------------------------------------------- 1 | import { ApiCommonErrorResponses, FilterArgs, RoleEnum, Roles } from '@lenne.tech/nest-server'; 2 | import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; 3 | import { ApiOkResponse } from '@nestjs/swagger'; 4 | 5 | import { <%= props.namePascal %>Service } from './<%= props.nameKebab %>.service'; 6 | import { <%= props.namePascal %>Input } from './inputs/<%= props.nameKebab %>.input'; 7 | import { <%= props.namePascal %>CreateInput } from './inputs/<%= props.nameKebab %>-create.input'; 8 | import { <%= props.namePascal %> } from './<%= props.nameKebab %>.model'; 9 | 10 | @ApiCommonErrorResponses() 11 | @Controller('<%= props.lowercase %>') 12 | @Roles(RoleEnum.ADMIN) 13 | export class <%= props.namePascal %>Controller { 14 | 15 | constructor(protected readonly <%= props.nameCamel %>Service: <%= props.namePascal %>Service) {} 16 | 17 | @Post() 18 | @Roles(RoleEnum.ADMIN) 19 | @ApiOkResponse({ type: <%= props.namePascal %> }) 20 | async create(@Body() input: <%= props.namePascal %>CreateInput): Promise<<%= props.namePascal %>> { 21 | return await this.<%= props.nameCamel %>Service.create(input); 22 | } 23 | 24 | @Get() 25 | @Roles(RoleEnum.ADMIN) 26 | @ApiOkResponse({ isArray: true, type: <%= props.namePascal %> }) 27 | async get(@Body() filterArgs: FilterArgs): Promise<<%= props.namePascal %>[]> { 28 | return await this.<%= props.nameCamel %>Service.find(filterArgs); 29 | } 30 | 31 | @Get(':id') 32 | @Roles(RoleEnum.ADMIN) 33 | @ApiOkResponse({ type: <%= props.namePascal %> }) 34 | async getById(@Param('id') id: string): Promise<<%= props.namePascal %>> { 35 | return await this.<%= props.nameCamel %>Service.findOne({filterQuery: { _id: id }}) 36 | } 37 | 38 | @Put(':id') 39 | @Roles(RoleEnum.ADMIN) 40 | @ApiOkResponse({ type: <%= props.namePascal %> }) 41 | async update(@Param('id') id: string, @Body() input: <%= props.namePascal %>Input): Promise<<%= props.namePascal %>> { 42 | return await this.<%= props.nameCamel %>Service.update(id, input); 43 | } 44 | 45 | @Delete(':id') 46 | @Roles(RoleEnum.ADMIN) 47 | @ApiOkResponse({ type: <%= props.namePascal %> }) 48 | async delete(@Param('id') id: string): Promise<<%= props.namePascal %>> { 49 | return await this.<%= props.nameCamel %>Service.delete(id); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/template.model.ts.ejs: -------------------------------------------------------------------------------- 1 | import { Restricted, RoleEnum, equalIds, mapClasses , UnifiedField} from '@lenne.tech/nest-server'; 2 | <% if (props.isGql) { %> 3 | import { ObjectType } from '@nestjs/graphql'; 4 | <% } %> 5 | import { Schema as MongooseSchema, Prop, SchemaFactory } from '@nestjs/mongoose'; 6 | import { Document, Schema } from 'mongoose';<%- props.imports %> 7 | 8 | import { PersistenceModel } from '../../common/models/persistence.model'; 9 | import { User } from '../user/user.model'; 10 | 11 | export type <%= props.namePascal %>Document = <%= props.namePascal %> & Document; 12 | 13 | /** 14 | * <%= props.namePascal %> model 15 | */ 16 | @Restricted(RoleEnum.ADMIN) 17 | <% if (props.isGql) { %> @ObjectType({ description: '<%= props.namePascal %>' }) <% } %> 18 | @MongooseSchema({ timestamps: true }) 19 | export class <%= props.namePascal %> extends PersistenceModel { 20 | 21 | // =================================================================================================================== 22 | // Properties 23 | // =================================================================================================================== 24 | <%- props.props %> 25 | 26 | // =================================================================================================================== 27 | // Methods 28 | // =================================================================================================================== 29 | 30 | /** 31 | * Initialize instance with default values instead of undefined 32 | */ 33 | override init() { 34 | super.init(); 35 | // this.propertyName = []; 36 | return this; 37 | } 38 | 39 | /** 40 | * Map input 41 | * 42 | * Hint: Non-primitive variables should always be mapped (see mapClasses / mapClassesAsync in ModelHelper) 43 | */ 44 | override map(input) { 45 | super.map(input); 46 | // return mapClasses(input, { propertyName: PropertyModel }, this); 47 | return <%- props.mappings %> 48 | } 49 | 50 | /** 51 | * Verification of the user's rights to access the properties of this object 52 | * 53 | * Check roles, prepare or remove properties 54 | * Return undefined if the whole object should not be returned or throw an exception to stop the whole request 55 | */ 56 | override securityCheck(user: User, force?: boolean) { 57 | // In force mode or for admins everything is allowed 58 | if (force || user?.hasRole(RoleEnum.ADMIN)) { 59 | return this; 60 | } 61 | 62 | // Usually only the creator has access to the object 63 | if (!equalIds(user, this.createdBy)) { 64 | return undefined; 65 | } 66 | 67 | // Check permissions for properties of this object and return the object afterward 68 | return this; 69 | } 70 | } 71 | 72 | export const <%= props.namePascal %>Schema = SchemaFactory.createForClass(<%= props.namePascal %>); 73 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/template.module.ts.ejs: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@lenne.tech/nest-server'; 2 | import { Module, forwardRef } from '@nestjs/common'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import { PubSub } from 'graphql-subscriptions'; 5 | 6 | import { UserModule } from '../user/user.module'; 7 | import { <%= props.namePascal %>, <%= props.namePascal %>Schema } from './<%= props.nameKebab %>.model'; 8 | <% if ((props.controller === 'GraphQL') || (props.controller === 'Both')) { -%> 9 | import { <%= props.namePascal %>Resolver } from './<%= props.nameKebab %>.resolver'; 10 | <% } -%> 11 | import { <%= props.namePascal %>Service } from './<%= props.nameKebab %>.service'; 12 | <% if ((props.controller === 'Rest') || (props.controller === 'Both')) { -%> 13 | import { <%= props.namePascal %>Controller } from './<%= props.nameKebab %>.controller'; 14 | <% } -%> 15 | 16 | /** 17 | * <%= props.namePascal %> module 18 | */ 19 | @Module({ 20 | <% if ((props.controller === 'Rest') || (props.controller === 'Both')) { -%> 21 | controllers: [<%= props.namePascal %>Controller], 22 | <% } -%> 23 | exports: [ MongooseModule, <% if ((props.controller === 'GraphQL') || (props.controller === 'Both')) { -%> <%= props.namePascal %>Resolver, <% } -%> <%= props.namePascal %>Service], 24 | imports: [ MongooseModule.forFeature([{ name: <%= props.namePascal %>.name, schema: <%= props.namePascal %>Schema }]), forwardRef(() => UserModule) ], 25 | providers: [ 26 | ConfigService, 27 | <% if ((props.controller === 'GraphQL') || (props.controller === 'Both')) { -%> 28 | <%= props.namePascal %>Resolver, 29 | <% } -%> 30 | <%= props.namePascal %>Service, 31 | { 32 | provide: 'PUB_SUB', 33 | useValue: new PubSub(), 34 | }, 35 | ], 36 | }) 37 | export class <%= props.namePascal %>Module {} 38 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/template.resolver.ts.ejs: -------------------------------------------------------------------------------- 1 | import { FilterArgs, GraphQLServiceOptions, RoleEnum, Roles, ServiceOptions } from '@lenne.tech/nest-server'; 2 | import { Inject } from '@nestjs/common'; 3 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 4 | import { PubSub } from 'graphql-subscriptions'; 5 | 6 | import { <%= props.namePascal %>Input } from './inputs/<%= props.nameKebab %>.input'; 7 | import { <%= props.namePascal %>CreateInput } from './inputs/<%= props.nameKebab %>-create.input'; 8 | import { FindAndCount<%= props.namePascal %>sResult } from './outputs/find-and-count-<%= props.nameKebab %>s-result.output'; 9 | import { <%= props.namePascal %> } from './<%= props.nameKebab %>.model'; 10 | import { <%= props.namePascal %>Service } from './<%= props.nameKebab %>.service'; 11 | 12 | /** 13 | * Resolver to process with <%= props.namePascal %> data 14 | */ 15 | @Roles(RoleEnum.ADMIN) 16 | @Resolver(() => <%= props.namePascal %>) 17 | export class <%= props.namePascal %>Resolver { 18 | 19 | /** 20 | * Import services 21 | */ 22 | constructor( 23 | private readonly <%= props.nameCamel %>Service: <%= props.namePascal %>Service, 24 | @Inject('PUB_SUB') protected readonly pubSub: PubSub, 25 | ) {} 26 | 27 | // =========================================================================== 28 | // Queries 29 | // =========================================================================== 30 | 31 | /** 32 | * Get and total count <%= props.namePascal %>s (via filter) 33 | */ 34 | @Roles(RoleEnum.S_USER) 35 | @Query(() => FindAndCount<%= props.namePascal %>sResult, { description: 'Find <%= props.namePascal %>s (via filter)' }) 36 | async findAndCount<%= props.namePascal %>s( 37 | @GraphQLServiceOptions({ gqlPath: 'findAndCount<%= props.namePascal %>s.items' }) serviceOptions: ServiceOptions, 38 | @Args() args?: FilterArgs, 39 | ) { 40 | return await this.<%= props.nameCamel %>Service.findAndCount(args, { 41 | ...serviceOptions, 42 | inputType: FilterArgs, 43 | }); 44 | } 45 | 46 | /** 47 | * Get <%= props.namePascal %>s (via filter) 48 | */ 49 | @Roles(RoleEnum.S_USER) 50 | @Query(() => [<%= props.namePascal %>], { description: 'Find <%= props.namePascal %>s (via filter)' }) 51 | async find<%= props.namePascal %>s( 52 | @GraphQLServiceOptions() serviceOptions: ServiceOptions, 53 | @Args() args?: FilterArgs, 54 | ) { 55 | return await this.<%= props.nameCamel %>Service.find(args, { 56 | ...serviceOptions, 57 | inputType: FilterArgs, 58 | }); 59 | } 60 | 61 | /** 62 | * Get <%= props.namePascal %> via ID 63 | */ 64 | @Roles(RoleEnum.S_USER) 65 | @Query(() => <%= props.namePascal %>, { description: 'Get <%= props.namePascal %> with specified ID' }) 66 | async get<%= props.namePascal %>( 67 | @GraphQLServiceOptions() serviceOptions: ServiceOptions, 68 | @Args('id') id: string, 69 | ): Promise<<%= props.namePascal %>> { 70 | return await this.<%= props.nameCamel %>Service.get(id, serviceOptions); 71 | } 72 | 73 | // =========================================================================== 74 | // Mutations 75 | // =========================================================================== 76 | 77 | /** 78 | * Create new <%= props.namePascal %> 79 | */ 80 | @Roles(RoleEnum.S_USER) 81 | @Mutation(() => <%= props.namePascal %>, { description: 'Create a new <%= props.namePascal %>' }) 82 | async create<%= props.namePascal %>( 83 | @GraphQLServiceOptions() serviceOptions: ServiceOptions, 84 | @Args('input') input: <%= props.namePascal %>CreateInput, 85 | ): Promise<<%= props.namePascal %>> { 86 | return await this.<%= props.nameCamel %>Service.create(input, { 87 | ...serviceOptions, 88 | inputType: <%= props.namePascal %>CreateInput, 89 | }); 90 | } 91 | 92 | /** 93 | * Delete existing <%= props.namePascal %> 94 | */ 95 | @Roles(RoleEnum.S_USER) 96 | @Mutation(() => <%= props.namePascal %>, { description: 'Delete existing <%= props.namePascal %>' }) 97 | async delete<%= props.namePascal %>( 98 | @GraphQLServiceOptions() serviceOptions: ServiceOptions, 99 | @Args('id') id: string, 100 | ): Promise<<%= props.namePascal %>> { 101 | return await this.<%= props.nameCamel %>Service.delete(id, { 102 | ...serviceOptions, 103 | roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR], 104 | }); 105 | } 106 | 107 | /** 108 | * Update existing <%= props.namePascal %> 109 | */ 110 | @Roles(RoleEnum.S_USER) 111 | @Mutation(() => <%= props.namePascal %>, { description: 'Update existing <%= props.namePascal %>' }) 112 | async update<%= props.namePascal %>( 113 | @GraphQLServiceOptions() serviceOptions: ServiceOptions, 114 | @Args('id') id: string, 115 | @Args('input') input: <%= props.namePascal %>Input, 116 | ): Promise<<%= props.namePascal %>> { 117 | return await this.<%= props.nameCamel %>Service.update(id, input, { 118 | ...serviceOptions, 119 | inputType: <%= props.namePascal %>Input, 120 | roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR], 121 | }); 122 | } 123 | 124 | // =========================================================================== 125 | // Subscriptions 126 | // =========================================================================== 127 | 128 | /** 129 | * Subscription for create <%= props.namePascal %> 130 | */ 131 | @Subscription(() => <%= props.namePascal %>, { 132 | filter(this: <%= props.namePascal %>Resolver, payload, variables, context) { 133 | return context?.user?.hasRole?.(RoleEnum.ADMIN); 134 | }, 135 | resolve: (value) => value, 136 | }) 137 | async <%= props.nameCamel %>Created() { 138 | return this.pubSub.asyncIterableIterator('<%= props.nameCamel %>Created'); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/templates/nest-server-module/template.service.ts.ejs: -------------------------------------------------------------------------------- 1 | import { ConfigService, CrudService, ServiceOptions, assignPlain } from '@lenne.tech/nest-server'; 2 | import { Inject, Injectable, NotFoundException } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | import { PubSub } from 'graphql-subscriptions'; 5 | import { Model } from 'mongoose'; 6 | 7 | import { <%= props.namePascal %>Input } from './inputs/<%= props.nameKebab %>.input'; 8 | import { <%= props.namePascal %>CreateInput } from './inputs/<%= props.nameKebab %>-create.input'; 9 | import { <%= props.namePascal %>, <%= props.namePascal %>Document } from './<%= props.nameKebab %>.model'; 10 | 11 | 12 | /** 13 | * <%= props.namePascal %> service 14 | */ 15 | @Injectable() 16 | export class <%= props.namePascal %>Service extends CrudService<<%= props.namePascal %>, <%= props.namePascal %>CreateInput, <%= props.namePascal %>Input> { 17 | 18 | // =================================================================================================================== 19 | // Properties 20 | // =================================================================================================================== 21 | 22 | // =================================================================================================================== 23 | // Injections 24 | // =================================================================================================================== 25 | 26 | /** 27 | * Constructor for injecting services 28 | * 29 | * Hints: 30 | * To resolve circular dependencies, integrate services as follows: 31 | * @Inject(forwardRef(() => XxxService)) protected readonly xxxService: WrapperType 32 | */ 33 | constructor( 34 | protected override readonly configService: ConfigService, 35 | @InjectModel('<%= props.namePascal %>') protected readonly <%= props.nameCamel %>Model: Model<<%= props.namePascal %>Document>, 36 | @Inject('PUB_SUB') protected readonly pubSub: PubSub, 37 | ) { 38 | super({ configService, mainDbModel: <%= props.nameCamel %>Model, mainModelConstructor: <%= props.namePascal %> }); 39 | } 40 | 41 | // =================================================================================================================== 42 | // Methods 43 | // =================================================================================================================== 44 | 45 | /** 46 | * Create new <%= props.namePascal %> 47 | * Overwrites create method from CrudService 48 | */ 49 | override async create(input: <%= props.namePascal %>CreateInput, serviceOptions?: ServiceOptions): Promise<<%= props.namePascal %>> { 50 | // Get new <%= props.namePascal %> 51 | const created<%= props.namePascal %> = await super.create(input, serviceOptions); 52 | 53 | // Inform subscriber 54 | if (serviceOptions?.pubSub === undefined || serviceOptions.pubSub) { 55 | await this.pubSub.publish('<%= props.nameCamel %>Created', <%= props.namePascal %>.map(created<%= props.namePascal %>)); 56 | } 57 | 58 | // Return created <%= props.namePascal %> 59 | return created<%= props.namePascal %>; 60 | } 61 | 62 | /** 63 | * Example method 64 | * Extends the CrudService 65 | */ 66 | async exampleMethod(id: string, input: Record, serviceOptions?: ServiceOptions): Promise<<%= props.namePascal %>> { 67 | 68 | // Get and check <%= props.namePascal %> 69 | const <%= props.nameCamel %> = await this.mainDbModel.findById(id).exec(); 70 | if (!<%= props.nameCamel %>) { 71 | throw new NotFoundException(`<%= props.namePascal %> not found with ID: ${id}`); 72 | } 73 | 74 | // Process input and output 75 | return await this.process(async (data) => { 76 | 77 | // Update, save and return <%= props.namePascal %> 78 | return await assignPlain(<%= props.nameCamel %>, data.input).save(); 79 | 80 | }, { input, serviceOptions }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/templates/nest-server-object/template-create.input.ts.ejs: -------------------------------------------------------------------------------- 1 | import { Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; 2 | import { InputType } from '@nestjs/graphql'; 3 | import { <%= props.namePascal %>Input } from './<%= props.nameKebab %>.input';<%- props.imports %> 4 | 5 | /** 6 | * <%= props.namePascal %> create input 7 | */ 8 | @Restricted(RoleEnum.ADMIN) 9 | @InputType({ description: 'Input data to create a new <%= props.namePascal %>' }) 10 | export class <%= props.namePascal %>CreateInput extends <%= props.namePascal %>Input { 11 | 12 | // =================================================================================================================== 13 | // Properties 14 | // =================================================================================================================== 15 | <%- props.props %> 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/nest-server-object/template.input.ts.ejs: -------------------------------------------------------------------------------- 1 | import { CoreInput, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; 2 | import { InputType } from '@nestjs/graphql';<%- props.imports %> 3 | 4 | /** 5 | * <%= props.namePascal %> input 6 | */ 7 | @Restricted(RoleEnum.ADMIN) 8 | @InputType({ description: 'Input data to update an existing <%= props.namePascal %>' }) 9 | export class <%= props.namePascal %>Input extends CoreInput { 10 | 11 | // =================================================================================================================== 12 | // Properties 13 | // =================================================================================================================== 14 | <%- props.props %> 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/nest-server-object/template.object.ts.ejs: -------------------------------------------------------------------------------- 1 | import { CoreModel, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; 2 | import { ObjectType } from '@nestjs/graphql'; 3 | import { Schema as MongooseSchema, Prop, SchemaFactory } from '@nestjs/mongoose'; 4 | import { Document } from 'mongoose';<%- props.imports %> 5 | 6 | import { User } from '../../../modules/user/user.model'; 7 | 8 | export type <%= props.namePascal %>Document = <%= props.namePascal %> & Document; 9 | 10 | /** 11 | * <%= props.namePascal %> model 12 | */ 13 | @Restricted(RoleEnum.ADMIN) 14 | @ObjectType({ description: '<%= props.namePascal %>' }) 15 | @MongooseSchema({ _id: false }) 16 | export class <%= props.namePascal %> extends CoreModel { 17 | 18 | // =================================================================================================================== 19 | // Properties 20 | // =================================================================================================================== 21 | <%- props.props %> 22 | 23 | // =================================================================================================================== 24 | // Methods 25 | // =================================================================================================================== 26 | 27 | /** 28 | * Initialize instance with default values instead of undefined 29 | */ 30 | override init() { 31 | super.init(); 32 | // this.xxx = []; 33 | return this; 34 | } 35 | 36 | /** 37 | * Map input 38 | * 39 | * Hint: Non-primitive variables should always be mapped (see mapClasses / mapClassesAsync in ModelHelper) 40 | */ 41 | override map(input) { 42 | super.map(input); 43 | // return mapClasses(input, { propertyName: PropertyModel }, this); 44 | return <%- props.mappings %> 45 | } 46 | 47 | /** 48 | * Verification of the user's rights to access the properties of this object 49 | */ 50 | override securityCheck(user: User, force?: boolean) { 51 | if (force || user?.hasRole(RoleEnum.ADMIN)) { 52 | return this; 53 | } 54 | // Check rights for properties of this object 55 | return this; 56 | } 57 | } 58 | 59 | export const <%= props.namePascal %>Schema = SchemaFactory.createForClass(<%= props.namePascal %>); 60 | -------------------------------------------------------------------------------- /src/templates/nest-server-starter/README.md.ejs: -------------------------------------------------------------------------------- 1 | # <%= props.name %> 2 | 3 | The <%= props.name %> is a backend server based on the [NestJS](https://nestjs.com/) framework. 4 | 5 | <%= props.description %> 6 | 7 | The server was initialized with the [Starter for lenne.Tech Nest Server](https://github.com/lenneTech/nest-server-starter). 8 | 9 | ## Requirements 10 | 11 | - [Node.js incl. npm](https://nodejs.org): 12 | the runtime environment for your server 13 | 14 | - [MongoDB](https://docs.mongodb.com/manual/installation/#mongodb-community-edition-installation-tutorials) 15 | the database for your objects 16 | 17 | 18 | ## Start the server 19 | 20 | ``` 21 | $ npm run start:dev 22 | ``` 23 | 24 | ## Extend the server 25 | 26 | This server is based on [lenne.Tech Nest Server](https://github.com/lenneTech/nest-server). 27 | 28 | Since the lenne.Tech Nest Server is based on [Nest](https://nestjs.com/), you can find all information about extending 29 | the **<%= props.name %>** in the [documentation of Nest](https://docs.nestjs.com/). 30 | 31 | To create a new Module with model, inputs, resolver and service you can use the [CLI](https://github.com/lenneTech/cli): 32 | 33 | ``` 34 | $ lt server module MODULE_NAME 35 | ``` 36 | 37 | The documentation of the extensions and auxiliary classes that the 38 | [lenne.Tech Nest Server](https://github.com/lenneTech/nest-server) contains is currently under construction. 39 | As long as this is not yet available,have a look at the 40 | [source code](https://github.com/lenneTech/nest-server/tree/master/src/core). 41 | There you will find a lot of things that will help you to extend your server, such as: 42 | 43 | - [GraphQL scalars](https://github.com/lenneTech/nest-server/tree/master/src/core/common/scalars) 44 | - [Filter and pagination](https://github.com/lenneTech/nest-server/tree/master/src/core/common/args) 45 | - [Decorators for restrictions and roles](https://github.com/lenneTech/nest-server/tree/master/src/core/common/decorators) 46 | - [Authorisation handling](https://github.com/lenneTech/nest-server/tree/master/src/core/modules/auth) 47 | - [Ready to use user module](https://github.com/lenneTech/nest-server/tree/master/src/core/modules/user) 48 | - [Common helpers](https://github.com/lenneTech/nest-server/tree/master/src/core/common/helpers) and 49 | [helpers for tests](https://github.com/lenneTech/nest-server/blob/master/src/test/test.helper.ts) 50 | - ... 51 | 52 | ## Further information 53 | 54 | ### Running the app 55 | 56 | ```bash 57 | # Development 58 | $ npm start 59 | 60 | # Watch mode 61 | $ npm run start:dev 62 | 63 | # Production mode 64 | $ npm run start:prod 65 | ``` 66 | 67 | ### Test 68 | 69 | ```bash 70 | # e2e tests 71 | $ npm run test:e2e 72 | ``` 73 | 74 | Configuration for testing: 75 | ``` 76 | Node interpreter: /user/local/bin/node 77 | Jest package: FULL_PATH_TO_PROJECT_DIR/node_modules/jest 78 | Working directory: FULL_PATH_TO_PROJECT_DIR 79 | Jest options: --config jest-e2e.json --forceExit 80 | ``` 81 | see [E2E-Tests.run.xml](.run/E2E-Tests.run.xml) 82 | 83 | ## Debugging 84 | 85 | Configuration for debugging is: 86 | ``` 87 | Node interpreter: /user/local/bin/node 88 | Node parameters: node_modules/@nestjs/cli/bin/nest.js start --debug --watch 89 | Working directory: FULL_PATH_TO_PROJECT_DIR 90 | JavaScript file: src/main.ts 91 | ``` 92 | see [Debug.run.xml](.run/Debug.run.xml) 93 | 94 | 95 | ## Test & debug the NestServer package in this project 96 | Use [yalc](https://github.com/wclr/yalc) to include the NestJS server in the project. 97 | 98 | 1. clone [NestServer](https://github.com/lenneTech/nest-server): `git clone https://github.com/lenneTech/nest-server.git` 99 | 2. go to the nest-server folder (`cd nest-server`), install the packages via `npm i` and start the nest server in watch & yalc mode: `npm run watch` 100 | 3. link the nest server live package to this project via `npm run link:nest-server` and start the server: `npm start` 101 | 4. unlink the nest-server live package and use the normal package again when you are done: `npm run unlink:nest-server` 102 | 103 | ## Deployment with deploy.party 104 | 105 | This project is prepared for deployment with deploy.party. 106 | 107 | Example configuration for deploy.party (productive): 108 | 109 | | Key | Value | 110 | |----------------------|----------------------------------------------------| 111 | | Source | GitLab | 112 | | Repository | my-repo | 113 | | Branch | main | 114 | | Registry | localhost | 115 | | Name | api | 116 | | URL | api.my-domain.com | 117 | | Type | Node | 118 | | Base image | node:20 | 119 | | Custom image command | RUN apt-get install -y tzdata curl | 120 | | | ENV TZ Europe/Berlin | 121 | | Base directory | ./projects/api | 122 | | Install command | npm install | 123 | | Build command | npm run build | 124 | | Start command | npm run dp:prod | 125 | | Healthcheck command | curl --fail http://localhost:3000/meta \|\| exit 1 | 126 | | Port | 3000 | 127 | | Enable SSL | true | 128 | 129 | ## Documentation 130 | The API and developer documentation can automatically be generated. 131 | 132 | ```bash 133 | # generate and serve documentation 134 | $ npm run docs 135 | ``` 136 | 137 | ## Update 138 | An update to a new Nest Sever version can be done as follows: 139 | 140 | 1. set the new Nest Server version in the package.json under `{dependencies: {"@lenne.tech/nest-server": "NEW_VERSION" }}`. 141 | 2. run `npm run update` 142 | 3. adjust project according to changes in git history from nest server 143 | 4. run tests via `npm run tests:e2e`, build via `npm run build` and start the server with `npm start` to check if everything is working 144 | 145 | Simply compare the current version in the Git history of 146 | [Starter for lenne.Tech Nest Server](https://github.com/lenneTech/nest-server-starter) with the version that was 147 | previously used in the project and adapt your own project accordingly. 148 | -------------------------------------------------------------------------------- /src/templates/nest-server-tests/tests.e2e-spec.ts.ejs: -------------------------------------------------------------------------------- 1 | import { RoleEnum, TestGraphQLType, TestHelper } from '@lenne.tech/nest-server'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import envConfig from '../src/config.env'; 5 | import { User } from '../src/server/modules/user/user.model'; 6 | import { UserService } from '../src/server/modules/user/user.service'; 7 | import { ServerModule } from '../src/server/server.module'; 8 | import { MongoClient, ObjectId } from 'mongodb'; 9 | 10 | describe('<%= props.namePascal %> (e2e)', () => { 11 | let app; 12 | let testHelper: TestHelper; 13 | 14 | // database 15 | let connection; 16 | let db; 17 | 18 | // Global vars 19 | let userService: UserService; 20 | const users: Partial[] = []; 21 | 22 | // =================================================================================================================== 23 | // Preparations 24 | // =================================================================================================================== 25 | 26 | /** 27 | * Before all tests 28 | */ 29 | beforeAll(async () => { 30 | try { 31 | const moduleFixture: TestingModule = await Test.createTestingModule({ 32 | imports: [ServerModule], 33 | providers: [ 34 | UserService, 35 | { 36 | provide: 'PUB_SUB', 37 | useValue: new PubSub(), 38 | }, 39 | ], 40 | }).compile(); 41 | app = moduleFixture.createNestApplication(); 42 | app.setBaseViewsDir(envConfig.templates.path); 43 | app.setViewEngine(envConfig.templates.engine); 44 | await app.init(); 45 | testHelper = new TestHelper(app); 46 | userService = moduleFixture.get(UserService); 47 | 48 | // Connection to database 49 | connection = await MongoClient.connect(envConfig.mongoose.uri); 50 | db = await connection.db(); 51 | } catch (e) { 52 | console.log('beforeAllError', e); 53 | } 54 | }); 55 | 56 | /** 57 | * After all tests are finished 58 | */ 59 | afterAll(async () => { 60 | await connection.close(); 61 | await app.close(); 62 | }); 63 | 64 | // =================================================================================================================== 65 | // Initialization tests 66 | // =================================================================================================================== 67 | 68 | /** 69 | * Create and verify users for testing 70 | */ 71 | it('createAndVerifyUsers', async () => { 72 | const userCount = 2; 73 | for (let i = 0; i < userCount; i++) { 74 | const random = Math.random().toString(36).substring(7); 75 | const input = { 76 | password: random, 77 | email: random + '@testusers.com', 78 | firstName: 'Test' + random, 79 | lastName: 'User' + random, 80 | }; 81 | 82 | // Sign up user 83 | const res: any = await testHelper.graphQl({ 84 | name: 'signUp', 85 | type: TestGraphQLType.MUTATION, 86 | arguments: { input }, 87 | fields: [{ user: ['id', 'email', 'firstName', 'lastName'] }], 88 | }); 89 | res.user.password = input.password; 90 | users.push(res.user); 91 | 92 | // Verify user 93 | await db.collection('users').updateOne({ _id: new ObjectId(res.id) }, { $set: { verified: true } }); 94 | } 95 | expect(users.length).toBeGreaterThanOrEqual(userCount); 96 | }); 97 | 98 | /** 99 | * Sign in users 100 | */ 101 | it('signInUsers', async () => { 102 | for (const user of users) { 103 | const res: any = await testHelper.graphQl({ 104 | name: 'signIn', 105 | arguments: { 106 | input: { 107 | email: user.email, 108 | password: user.password, 109 | }, 110 | }, 111 | fields: ['token', { user: ['id', 'email'] }], 112 | }); 113 | expect(res.user.id).toEqual(user.id); 114 | expect(res.user.email).toEqual(user.email); 115 | user.token = res.token; 116 | } 117 | }); 118 | 119 | /** 120 | * Prepare users 121 | */ 122 | it('prepareUsers', async () => { 123 | await db 124 | .collection('users') 125 | .findOneAndUpdate({ _id: new ObjectId(users[0].id) }, { $set: { roles: [RoleEnum.ADMIN] } }); 126 | }); 127 | 128 | // =================================================================================================================== 129 | // Tests for <%= props.namePascal %> 130 | // =================================================================================================================== 131 | 132 | /** 133 | * Test 134 | */ 135 | it('test', async () => { 136 | console.log('Implement <%= props.namePascal %> test here'); 137 | }); 138 | 139 | // =================================================================================================================== 140 | // Clean up tests 141 | // =================================================================================================================== 142 | 143 | /** 144 | * Delete users 145 | */ 146 | it('deleteUsers', async () => { 147 | // Add admin role to user 2 148 | await userService.setRoles(users[1].id, ['admin']); 149 | 150 | for (const user of users) { 151 | const res: any = await testHelper.graphQl( 152 | { 153 | name: 'deleteUser', 154 | type: TestGraphQLType.MUTATION, 155 | arguments: { 156 | id: user.id, 157 | }, 158 | fields: ['id'], 159 | }, 160 | { token: users[1].token } 161 | ); 162 | expect(res.id).toEqual(user.id); 163 | } 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/templates/typescript-starter/README.md.ejs: -------------------------------------------------------------------------------- 1 | # <%= props.name %> 2 | 3 | [![License](https://img.shields.io/github/license/<%= props.nameKebab %>)](/LICENSE) [![CircleCI](https://circleci.com/gh/<%= props.nameKebab %>/tree/master.svg?style=shield)](https://circleci.com/gh/<%= props.nameKebab %>/tree/master) 4 | [![Dependency Status](https://david-dm.org/<%= props.nameKebab %>.svg)](https://david-dm.org/<%= props.nameKebab %>) [![devDependency Status](https://david-dm.org/<%= props.nameKebab %>/dev-status.svg)](https://david-dm.org/<%= props.nameKebab %>?type=dev) 5 | 6 | 9 | 10 | 11 | ## Requirements 12 | 13 | - [Node.js incl. npm](https://nodejs.org): 14 | the runtime environment for your project 15 | 16 | 17 | ## Scripts 18 | 19 | ```bash 20 | # Lint 21 | $ npm run lint 22 | 23 | # Test 24 | $ npm test 25 | $ npm run watch 26 | $ npm run coverage 27 | 28 | # Build 29 | $ npm build 30 | ``` 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "experimentalDecorators": true, 5 | "lib": [ 6 | "es2015", 7 | "scripthost", 8 | "es2015.promise", 9 | "es2015.generator", 10 | "es2015.iterable", 11 | "dom" 12 | ], 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "noImplicitAny": false, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "sourceMap": false, 19 | "inlineSourceMap": true, 20 | "outDir": "build", 21 | "strict": false, 22 | "target": "es6", 23 | "resolveJsonModule": true 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": ["node_modules", "src/templates/**/*"] 27 | } 28 | --------------------------------------------------------------------------------