├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── bin ├── run └── run.cmd ├── nxpm-plugins.gif ├── nxpm-projects.gif ├── nxpm-sandbox.gif ├── package.json ├── src ├── commands │ ├── config │ │ ├── delete.ts │ │ ├── edit.ts │ │ ├── get.ts │ │ └── set.ts │ ├── plugins.ts │ ├── projects.ts │ ├── registry │ │ ├── disable.ts │ │ ├── enable.ts │ │ ├── start.ts │ │ └── status.ts │ ├── release.ts │ ├── sandbox.ts │ └── sandbox │ │ └── pull.ts ├── global.d.ts ├── index.ts ├── lib │ ├── config │ │ ├── config.ts │ │ └── utils │ │ │ └── config-utils.ts │ ├── plugins │ │ ├── interfaces │ │ │ └── plugin-config.ts │ │ ├── plugins.ts │ │ └── utils │ │ │ └── plugin-utils.ts │ ├── projects │ │ └── projects.ts │ ├── release │ │ ├── interfaces │ │ │ ├── release-config.ts │ │ │ ├── validated-config.ts │ │ │ ├── validated-packages.ts │ │ │ └── validated-workspace.ts │ │ ├── release-validate.ts │ │ └── release.ts │ ├── sandbox │ │ ├── interfaces │ │ │ ├── sandbox-config.ts │ │ │ ├── sandbox-pull-config.ts │ │ │ └── sandbox.ts │ │ ├── sandbox-pull.ts │ │ ├── sandbox.ts │ │ └── utils │ │ │ └── sandbox-utils.ts │ └── verdaccio │ │ └── index.ts └── utils │ ├── base-command.ts │ ├── base-config.ts │ ├── constants.ts │ ├── get-workspace-info.ts │ ├── index.ts │ ├── logging.ts │ ├── parse-version.ts │ ├── user-config.ts │ ├── utils.ts │ └── vendor │ └── nx-console │ ├── read-schematic-collections.ts │ ├── schema.ts │ └── utils.ts ├── test ├── commands │ ├── config │ │ ├── delete.test.ts │ │ ├── edit.test.ts │ │ ├── get.test.ts │ │ └── set.test.ts │ ├── plugins.test.ts │ ├── projects.test.ts │ ├── registry │ │ ├── disable.test.ts │ │ ├── enable.test.ts │ │ ├── start.test.ts │ │ └── status.test.ts │ ├── release.test.ts │ ├── sandbox.test.ts │ └── sandbox │ │ └── pull.test.ts ├── mocha.opts └── tsconfig.json ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | jobs: 4 | node-latest: &test 5 | docker: 6 | - image: node:latest 7 | working_directory: ~/cli 8 | steps: 9 | - checkout 10 | - restore_cache: &restore_cache 11 | keys: 12 | - v1-npm-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}} 13 | - v1-npm-{{checksum ".circleci/config.yml"}} 14 | - run: 15 | name: Install dependencies 16 | command: yarn 17 | - run: ./bin/run --version 18 | - run: ./bin/run --help 19 | - run: 20 | name: Testing 21 | command: yarn test 22 | node-12: 23 | <<: *test 24 | docker: 25 | - image: node:12 26 | node-10: 27 | <<: *test 28 | docker: 29 | - image: node:10 30 | cache: 31 | <<: *test 32 | steps: 33 | - checkout 34 | - run: 35 | name: Install dependencies 36 | command: yarn 37 | - save_cache: 38 | key: v1-npm-{{checksum ".circleci/config.yml"}}-{{checksum "yarn.lock"}} 39 | paths: 40 | - ~/cli/node_modules 41 | - /usr/local/share/.cache/yarn 42 | - /usr/local/share/.config/yarn 43 | 44 | workflows: 45 | version: 2 46 | "nxpm": 47 | jobs: 48 | - node-latest 49 | - node-12 50 | - node-10 51 | - cache: 52 | filters: 53 | tags: 54 | only: /^v.*/ 55 | branches: 56 | ignore: /.*/ 57 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ], 6 | "rules": { 7 | "object-curly-spacing": "off", 8 | "no-console": "off", 9 | "no-else-return": "off", 10 | "no-implicit-coercion": "off", 11 | "indent": "off", 12 | "arrow-parens": "off", 13 | "operator-linebreak": "off", 14 | "no-process-exit": "off", 15 | "unicorn/catch-error-name": "off", 16 | "unicorn/explicit-length-check": "off", 17 | "unicorn/no-process-exit": "off", 18 | "@typescript-eslint/member-delimiter-style": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | oclif.manifest.json 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | /tmp 6 | apps/api/src/schema.graphql 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "semi": false, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Bram Borggreve https://github.com/beeman 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nxpm 2 | 3 | ## Looking for the full-stack generator? 4 | 5 | ### ➡️ Check here [github.com/nxpm/stack](https://github.com/nxpm/stack) 6 | 7 |

8 | 9 |

CLI to make the world-class nx workspace even more amazing!

10 | 11 |

nxpm.dev

12 | 13 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 14 | [![Version](https://img.shields.io/npm/v/nxpm.svg)](https://npmjs.org/package/nxpm) 15 | [![CircleCI](https://circleci.com/gh/nxpm/nxpm/tree/master.svg?style=shield)](https://circleci.com/gh/nxpm/nxpm/tree/master) 16 | [![Downloads/week](https://img.shields.io/npm/dw/nxpm.svg)](https://npmjs.org/package/nxpm) 17 | [![License](https://img.shields.io/npm/l/nxpm.svg)](https://github.com/nxpm/nxpm/blob/master/package.json) 18 | 19 | 20 | * [nxpm](#nxpm) 21 | * [Usage](#usage) 22 | * [Commands](#commands) 23 | 24 | 25 | ### nxpm plugins 26 | 27 | Interactively install and remove plugins, run schematics from installed plugins. 28 | 29 |

30 | 31 | ### nxpm projects 32 | 33 | Interactively browse the projects in a workspace and run builders and schematics. 34 | 35 |

36 | 37 | ### nxpm sandbox 38 | 39 | Quickly spin up Docker based sandboxes with various NX presets installed. 40 | 41 |

42 | 43 | # Usage 44 | 45 | 46 | ```sh-session 47 | $ npm install -g nxpm 48 | $ nxpm COMMAND 49 | running command... 50 | $ nxpm (-v|--version|version) 51 | nxpm/2.0.0 darwin-x64 node-v16.13.0 52 | $ nxpm --help [COMMAND] 53 | USAGE 54 | $ nxpm COMMAND 55 | ... 56 | ``` 57 | 58 | 59 | # Commands 60 | 61 | 62 | * [`nxpm config:delete`](#nxpm-configdelete) 63 | * [`nxpm config:edit`](#nxpm-configedit) 64 | * [`nxpm config:get KEY`](#nxpm-configget-key) 65 | * [`nxpm config:set KEY VALUE`](#nxpm-configset-key-value) 66 | * [`nxpm help [COMMAND]`](#nxpm-help-command) 67 | * [`nxpm plugins`](#nxpm-plugins) 68 | * [`nxpm projects [PROJECTNAME] [TARGET]`](#nxpm-projects-projectname-target) 69 | * [`nxpm registry:disable`](#nxpm-registrydisable) 70 | * [`nxpm registry:enable`](#nxpm-registryenable) 71 | * [`nxpm registry:start`](#nxpm-registrystart) 72 | * [`nxpm registry:status`](#nxpm-registrystatus) 73 | * [`nxpm release [VERSION]`](#nxpm-release-version) 74 | * [`nxpm sandbox [SANDBOXID] [ACTION]`](#nxpm-sandbox-sandboxid-action) 75 | * [`nxpm sandbox:pull`](#nxpm-sandboxpull) 76 | 77 | ## `nxpm config:delete` 78 | 79 | Delete the config file 80 | 81 | ``` 82 | USAGE 83 | $ nxpm config:delete 84 | 85 | OPTIONS 86 | -g, --global (required) Global config 87 | -h, --help show CLI help 88 | ``` 89 | 90 | _See code: [src/commands/config/delete.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/config/delete.ts)_ 91 | 92 | ## `nxpm config:edit` 93 | 94 | Edit the config file 95 | 96 | ``` 97 | USAGE 98 | $ nxpm config:edit 99 | 100 | OPTIONS 101 | -g, --global (required) Global config 102 | -h, --help show CLI help 103 | ``` 104 | 105 | _See code: [src/commands/config/edit.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/config/edit.ts)_ 106 | 107 | ## `nxpm config:get KEY` 108 | 109 | describe the command here 110 | 111 | ``` 112 | USAGE 113 | $ nxpm config:get KEY 114 | 115 | OPTIONS 116 | -g, --global (required) Global config 117 | -h, --help show CLI help 118 | ``` 119 | 120 | _See code: [src/commands/config/get.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/config/get.ts)_ 121 | 122 | ## `nxpm config:set KEY VALUE` 123 | 124 | describe the command here 125 | 126 | ``` 127 | USAGE 128 | $ nxpm config:set KEY VALUE 129 | 130 | OPTIONS 131 | -g, --global (required) Global config 132 | -h, --help show CLI help 133 | ``` 134 | 135 | _See code: [src/commands/config/set.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/config/set.ts)_ 136 | 137 | ## `nxpm help [COMMAND]` 138 | 139 | display help for nxpm 140 | 141 | ``` 142 | USAGE 143 | $ nxpm help [COMMAND] 144 | 145 | ARGUMENTS 146 | COMMAND command to show help for 147 | 148 | OPTIONS 149 | --all see all commands in CLI 150 | ``` 151 | 152 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.0.1/src/commands/help.ts)_ 153 | 154 | ## `nxpm plugins` 155 | 156 | Install and remove community plugins 157 | 158 | ``` 159 | USAGE 160 | $ nxpm plugins 161 | 162 | OPTIONS 163 | -c, --cwd=cwd [default: /Users/beeman/nxpm/nxpm-cli] Current working directory 164 | -h, --help show CLI help 165 | -r, --refresh Refresh the list of plugins 166 | 167 | ALIASES 168 | $ nxpm pl 169 | ``` 170 | 171 | _See code: [src/commands/plugins.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/plugins.ts)_ 172 | 173 | ## `nxpm projects [PROJECTNAME] [TARGET]` 174 | 175 | Interactive menu to run builders and schematics for projects 176 | 177 | ``` 178 | USAGE 179 | $ nxpm projects [PROJECTNAME] [TARGET] 180 | 181 | ARGUMENTS 182 | PROJECTNAME The name of the project you want to operate on 183 | TARGET The target to run (build, serve, test, etc) 184 | 185 | OPTIONS 186 | -c, --cwd=cwd [default: /Users/beeman/nxpm/nxpm-cli] Current working directory 187 | -h, --help show CLI help 188 | 189 | ALIASES 190 | $ nxpm p 191 | ``` 192 | 193 | _See code: [src/commands/projects.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/projects.ts)_ 194 | 195 | ## `nxpm registry:disable` 196 | 197 | Disable yarn and npm from using local npm registry 198 | 199 | ``` 200 | USAGE 201 | $ nxpm registry:disable 202 | ``` 203 | 204 | _See code: [src/commands/registry/disable.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/registry/disable.ts)_ 205 | 206 | ## `nxpm registry:enable` 207 | 208 | Configure yarn and npm to use the local registry 209 | 210 | ``` 211 | USAGE 212 | $ nxpm registry:enable 213 | ``` 214 | 215 | _See code: [src/commands/registry/enable.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/registry/enable.ts)_ 216 | 217 | ## `nxpm registry:start` 218 | 219 | Start local npm registry 220 | 221 | ``` 222 | USAGE 223 | $ nxpm registry:start 224 | ``` 225 | 226 | _See code: [src/commands/registry/start.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/registry/start.ts)_ 227 | 228 | ## `nxpm registry:status` 229 | 230 | Show yarn and npm registry configuration 231 | 232 | ``` 233 | USAGE 234 | $ nxpm registry:status 235 | ``` 236 | 237 | _See code: [src/commands/registry/status.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/registry/status.ts)_ 238 | 239 | ## `nxpm release [VERSION]` 240 | 241 | Release publishable packages in an Nx Workspace 242 | 243 | ``` 244 | USAGE 245 | $ nxpm release [VERSION] 246 | 247 | ARGUMENTS 248 | VERSION The version you want to release in semver format (eg: 1.2.3-beta.4) 249 | 250 | OPTIONS 251 | -b, --build Build libraries after versioning 252 | -c, --cwd=cwd [default: /Users/beeman/nxpm/nxpm-cli] Current working directory 253 | -d, --dry-run Dry run, don't make permanent changes 254 | -f, --fix Automatically fix known issues 255 | -h, --help show CLI help 256 | -i, --allow-ivy Allow publishing Angular packages built for Ivy 257 | --ci CI mode (fully automatic release) 258 | --local Release package to local registry 259 | --localUrl=localUrl [default: http://localhost:4873/] URL to local registry 260 | ``` 261 | 262 | _See code: [src/commands/release.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/release.ts)_ 263 | 264 | ## `nxpm sandbox [SANDBOXID] [ACTION]` 265 | 266 | Create a sandbox using Docker 267 | 268 | ``` 269 | USAGE 270 | $ nxpm sandbox [SANDBOXID] [ACTION] 271 | 272 | ARGUMENTS 273 | SANDBOXID The ID of the sandbox 274 | ACTION Action to perform on sandbox 275 | 276 | OPTIONS 277 | -c, --cwd=cwd [default: /Users/beeman/nxpm/nxpm-cli] Current working directory 278 | -h, --help show CLI help 279 | -r, --refresh Refresh the list of plugins 280 | --port-api=port-api [default: 3000] Port to open for the API app 281 | --port-web=port-web [default: 4200] Port to open for the Web app 282 | --ports=ports Comma-separated list of additional ports to open (eg: 8080, 10080:80) 283 | ``` 284 | 285 | _See code: [src/commands/sandbox.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/sandbox.ts)_ 286 | 287 | ## `nxpm sandbox:pull` 288 | 289 | Pull images of sandboxes 290 | 291 | ``` 292 | USAGE 293 | $ nxpm sandbox:pull 294 | 295 | OPTIONS 296 | -f, --force Force removal of the sandboxes 297 | -h, --help show CLI help 298 | -m, --remove Remove all of the sandboxes before pulling 299 | -r, --refresh Refresh the list of sandboxes 300 | ``` 301 | 302 | _See code: [src/commands/sandbox/pull.ts](https://github.com/nxpm/nxpm-cli/blob/v2.0.0/src/commands/sandbox/pull.ts)_ 303 | 304 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /nxpm-plugins.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nxpm/nxpm-cli/644e446a2babf32beb25eaf0e14c215a7d7e03cf/nxpm-plugins.gif -------------------------------------------------------------------------------- /nxpm-projects.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nxpm/nxpm-cli/644e446a2babf32beb25eaf0e14c215a7d7e03cf/nxpm-projects.gif -------------------------------------------------------------------------------- /nxpm-sandbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nxpm/nxpm-cli/644e446a2babf32beb25eaf0e14c215a7d7e03cf/nxpm-sandbox.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nxpm", 3 | "description": "nxpm cli", 4 | "version": "2.0.0", 5 | "author": "Bram Borggreve @beeman", 6 | "bin": { 7 | "nxpm": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/nxpm/nxpm-cli/issues", 10 | "dependencies": { 11 | "@angular-devkit/core": "^9.1.6", 12 | "@angular-devkit/schematics": "^9.1.6", 13 | "@angular/cli": "^9.1.6", 14 | "@angular/compiler": "^9.1.7", 15 | "@angular/compiler-cli": "^9.1.7", 16 | "@nrwl/workspace": "^14.1.4", 17 | "@oclif/command": "^1.6.1", 18 | "@oclif/config": "^1.15.1", 19 | "@oclif/plugin-help": "^3.0.1", 20 | "chalk": "^4.0.0", 21 | "cli-ux": "^5.4.5", 22 | "fs-extra": "^9.0.0", 23 | "inquirer": "^7.1.0", 24 | "json5": "^2.1.3", 25 | "lodash": "^4.17.15", 26 | "node-fetch": "^2.6.0", 27 | "release-it": "^13.5.8", 28 | "tslib": "^1.13.0" 29 | }, 30 | "devDependencies": { 31 | "@nrwl/devkit": "^14.1.4", 32 | "@nrwl/tao": "^14.1.4", 33 | "@oclif/dev-cli": "^1.22.2", 34 | "@oclif/test": "^1.2.6", 35 | "@types/chai": "^4.2.11", 36 | "@types/fs-extra": "^8.1.0", 37 | "@types/inquirer": "^6.5.0", 38 | "@types/json5": "^0.0.30", 39 | "@types/mocha": "^7.0.2", 40 | "@types/node": "^14.0.1", 41 | "@types/node-fetch": "^2.5.7", 42 | "@types/tmp": "^0.2.0", 43 | "chai": "^4.2.0", 44 | "eslint": "^5.13", 45 | "eslint-config-oclif": "^3.1.0", 46 | "eslint-config-oclif-typescript": "^0.1.0", 47 | "globby": "^11.0.0", 48 | "husky": "^4.2.5", 49 | "lint-staged": "^10.2.2", 50 | "mocha": "^7.1.2", 51 | "nx": "^14.6.5", 52 | "nyc": "^15.0.1", 53 | "ts-node": "^8.10.1", 54 | "typescript": "4.4.4" 55 | }, 56 | "engines": { 57 | "node": ">=12.0.0" 58 | }, 59 | "files": [ 60 | "/bin", 61 | "/lib", 62 | "/npm-shrinkwrap.json", 63 | "/oclif.manifest.json" 64 | ], 65 | "homepage": "https://nxpm.dev/", 66 | "keywords": [ 67 | "oclif" 68 | ], 69 | "license": "MIT", 70 | "main": "lib/index.js", 71 | "oclif": { 72 | "commands": "./lib/commands", 73 | "bin": "nxpm", 74 | "plugins": [ 75 | "@oclif/plugin-help" 76 | ] 77 | }, 78 | "repository": "nxpm/nxpm-cli", 79 | "scripts": { 80 | "build": "npx tsc", 81 | "postpack": "rm -f oclif.manifest.json", 82 | "posttest": "eslint . --ext .ts --config .eslintrc", 83 | "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", 84 | "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", 85 | "version": "oclif-dev readme && git add README.md" 86 | }, 87 | "types": "lib/index.d.ts" 88 | } 89 | -------------------------------------------------------------------------------- /src/commands/config/delete.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { deleteConfig } from '../../lib/config/config' 3 | import { BaseCommand } from '../../utils' 4 | 5 | export default class ConfigDelete extends BaseCommand { 6 | static description = 'Delete the config file' 7 | 8 | static flags = { 9 | help: flags.help({ char: 'h' }), 10 | global: flags.boolean({ char: 'g', description: 'Global config', required: true }), 11 | } 12 | 13 | static args = [] 14 | 15 | async run() { 16 | const { flags } = this.parse(ConfigDelete) 17 | 18 | await deleteConfig({ 19 | global: flags.global, 20 | userConfig: this.userConfig, 21 | config: this.config, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/config/edit.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { editConfig } from '../../lib/config/config' 3 | import { BaseCommand } from '../../utils' 4 | 5 | export default class ConfigEdit extends BaseCommand { 6 | static description = 'Edit the config file' 7 | 8 | static flags = { 9 | help: flags.help({ char: 'h' }), 10 | global: flags.boolean({ char: 'g', description: 'Global config', required: true }), 11 | } 12 | 13 | static args = [] 14 | 15 | async run() { 16 | const { flags } = this.parse(ConfigEdit) 17 | 18 | await editConfig({ 19 | global: flags.global, 20 | userConfig: this.userConfig, 21 | config: this.config, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/config/get.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { getConfigParam } from '../../lib/config/config' 3 | import { BaseCommand } from '../../utils' 4 | 5 | export default class ConfigGet extends BaseCommand { 6 | static description = 'describe the command here' 7 | 8 | static flags = { 9 | help: flags.help({ char: 'h' }), 10 | global: flags.boolean({ char: 'g', description: 'Global config', required: true }), 11 | } 12 | 13 | static args = [ 14 | { 15 | name: 'key', 16 | required: true, 17 | }, 18 | ] 19 | 20 | async run() { 21 | const { args, flags } = this.parse(ConfigGet) 22 | 23 | await getConfigParam({ 24 | global: flags.global, 25 | key: args.key, 26 | userConfig: this.userConfig, 27 | config: this.config, 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/config/set.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { setConfigParam } from '../../lib/config/config' 3 | import { BaseCommand } from '../../utils' 4 | 5 | export default class ConfigGet extends BaseCommand { 6 | static description = 'describe the command here' 7 | 8 | static flags = { 9 | help: flags.help({ char: 'h' }), 10 | // flag with a value (-n, --name=VALUE) 11 | global: flags.boolean({ char: 'g', description: 'Global config', required: true }), 12 | } 13 | 14 | static args = [ 15 | { 16 | name: 'key', 17 | required: true, 18 | }, 19 | { name: 'value', required: true }, 20 | ] 21 | 22 | async run() { 23 | const { args, flags } = this.parse(ConfigGet) 24 | 25 | await setConfigParam({ 26 | global: flags.global, 27 | key: args.key, 28 | value: args.value, 29 | userConfig: this.userConfig, 30 | config: this.config, 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/plugins.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { plugins } from '../lib/plugins/plugins' 3 | import { BaseCommand } from '../utils' 4 | 5 | export default class Plugins extends BaseCommand { 6 | static aliases = ['pl'] 7 | 8 | static description = 'Install and remove community plugins' 9 | 10 | static flags = { 11 | cwd: flags.string({ 12 | char: 'c', 13 | description: 'Current working directory', 14 | default: process.cwd(), 15 | }), 16 | help: flags.help({ char: 'h' }), 17 | refresh: flags.boolean({ 18 | char: 'r', 19 | description: 'Refresh the list of plugins', 20 | default: false, 21 | }), 22 | } 23 | 24 | async run() { 25 | const { flags } = this.parse(Plugins) 26 | 27 | await plugins({ 28 | cwd: flags.cwd, 29 | userConfig: this.userConfig, 30 | refresh: flags.refresh, 31 | config: this.config, 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/projects.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { projects } from '../lib/projects/projects' 3 | import { BaseCommand } from '../utils' 4 | 5 | export default class Projects extends BaseCommand { 6 | static aliases = ['p'] 7 | 8 | static description = 'Interactive menu to run builders and schematics for projects' 9 | 10 | static flags = { 11 | cwd: flags.string({ 12 | char: 'c', 13 | description: 'Current working directory', 14 | default: process.cwd(), 15 | }), 16 | help: flags.help({ char: 'h' }), 17 | } 18 | 19 | static args = [ 20 | { 21 | name: 'projectName', 22 | description: 'The name of the project you want to operate on', 23 | required: false, 24 | }, 25 | { 26 | name: 'target', 27 | description: 'The target to run (build, serve, test, etc)', 28 | required: false, 29 | }, 30 | ] 31 | 32 | async run() { 33 | const { args, flags } = this.parse(Projects) 34 | 35 | await projects( 36 | { cwd: flags.cwd, dryRun: false, userConfig: this.userConfig, config: this.config }, 37 | args.projectName, 38 | args.target, 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/registry/disable.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { disableRegistry } from '../../lib/verdaccio' 3 | 4 | export default class RegistryDisable extends Command { 5 | static description = 'Disable yarn and npm from using local npm registry' 6 | 7 | async run() { 8 | disableRegistry() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/registry/enable.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { enableRegistry } from '../../lib/verdaccio' 3 | 4 | export default class RegistryEnable extends Command { 5 | static description = 'Configure yarn and npm to use the local registry' 6 | 7 | async run() { 8 | enableRegistry() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/registry/start.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { startRegistry } from '../../lib/verdaccio' 3 | 4 | export default class RegistryStart extends Command { 5 | static description = 'Start local npm registry' 6 | 7 | async run() { 8 | startRegistry() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/registry/status.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { registryStatus } from '../../lib/verdaccio' 3 | 4 | export default class RegistryStatus extends Command { 5 | static description = 'Show yarn and npm registry configuration' 6 | 7 | async run() { 8 | registryStatus() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/release.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import * as inquirer from 'inquirer' 3 | import { release } from '../lib/release/release' 4 | import { BaseCommand, log } from '../utils' 5 | import { parseVersion } from '../utils/parse-version' 6 | 7 | export default class Release extends BaseCommand { 8 | static description = 'Release publishable packages in an Nx Workspace' 9 | 10 | static flags = { 11 | build: flags.boolean({ char: 'b', description: 'Build libraries after versioning' }), 12 | ci: flags.boolean({ 13 | description: 'CI mode (fully automatic release)', 14 | default: false, 15 | }), 16 | cwd: flags.string({ 17 | char: 'c', 18 | description: 'Current working directory', 19 | default: process.cwd(), 20 | }), 21 | 'dry-run': flags.boolean({ char: 'd', description: "Dry run, don't make permanent changes" }), 22 | help: flags.help({ char: 'h' }), 23 | fix: flags.boolean({ char: 'f', description: 'Automatically fix known issues' }), 24 | local: flags.boolean({ 25 | description: 'Release package to local registry', 26 | default: false, 27 | }), 28 | localUrl: flags.string({ 29 | description: 'URL to local registry', 30 | default: 'http://localhost:4873/', 31 | }), 32 | } 33 | 34 | static args = [ 35 | { 36 | name: 'version', 37 | description: 'The version you want to release in semver format (eg: 1.2.3-beta.4)', 38 | required: false, 39 | }, 40 | ] 41 | 42 | async run() { 43 | const { args, flags } = this.parse(Release) 44 | 45 | if (!args.version) { 46 | const response = await inquirer.prompt([ 47 | { 48 | name: 'version', 49 | type: 'input', 50 | message: 'What version do you want to release?', 51 | validate(version: string): boolean | string { 52 | if (!parseVersion(version).isValid) { 53 | return 'Please use a valid semver version (eg: 1.2.3-beta.4)' 54 | } 55 | return true 56 | }, 57 | }, 58 | ]) 59 | args.version = response.version 60 | } 61 | 62 | if (this.userConfig?.release?.github?.token) { 63 | log('GITHUB_TOKEN', 'Using token from config file') 64 | process.env.GITHUB_TOKEN = this.userConfig?.release?.github?.token 65 | } 66 | 67 | await release({ 68 | build: flags.build, 69 | ci: flags.ci, 70 | config: this.config, 71 | cwd: flags.cwd, 72 | dryRun: flags['dry-run'], 73 | fix: flags.fix, 74 | version: args.version, 75 | local: flags.local, 76 | localUrl: flags.localUrl, 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/sandbox.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { sandbox } from '../lib/sandbox/sandbox' 3 | import { BaseCommand } from '../utils' 4 | 5 | export default class Sandbox extends BaseCommand { 6 | static description = 'Create a sandbox using Docker' 7 | 8 | static flags = { 9 | cwd: flags.string({ 10 | char: 'c', 11 | description: 'Current working directory', 12 | default: process.cwd(), 13 | }), 14 | help: flags.help({ char: 'h' }), 15 | refresh: flags.boolean({ 16 | char: 'r', 17 | description: 'Refresh the list of plugins', 18 | default: false, 19 | }), 20 | 'port-api': flags.string({ 21 | description: 'Port to open for the API app', 22 | default: '3000', 23 | }), 24 | 'port-web': flags.string({ 25 | description: 'Port to open for the Web app', 26 | default: '4200', 27 | }), 28 | ports: flags.string({ 29 | description: 'Comma-separated list of additional ports to open (eg: 8080, 10080:80)', 30 | default: '', 31 | }), 32 | } 33 | 34 | static args = [ 35 | { 36 | name: 'sandboxId', 37 | description: 'The ID of the sandbox', 38 | required: false, 39 | }, 40 | { 41 | name: 'action', 42 | description: 'Action to perform on sandbox', 43 | required: false, 44 | }, 45 | ] 46 | 47 | async run() { 48 | const { args, flags } = this.parse(Sandbox) 49 | 50 | await sandbox({ 51 | action: args.action, 52 | config: this.config, 53 | refresh: flags.refresh, 54 | portApi: flags['port-api'], 55 | portWeb: flags['port-web'], 56 | ports: flags.ports, 57 | sandboxId: args.sandboxId, 58 | userConfig: this.userConfig, 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/sandbox/pull.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { sandboxPull } from '../../lib/sandbox/sandbox-pull' 3 | import { BaseCommand } from '../../utils' 4 | 5 | export default class SandboxPull extends BaseCommand { 6 | static description = 'Pull images of sandboxes' 7 | 8 | static flags = { 9 | force: flags.boolean({ 10 | char: 'f', 11 | description: 'Force removal of the sandboxes', 12 | default: false, 13 | }), 14 | help: flags.help({ char: 'h' }), 15 | refresh: flags.boolean({ 16 | char: 'r', 17 | description: 'Refresh the list of sandboxes', 18 | default: false, 19 | }), 20 | remove: flags.boolean({ 21 | char: 'm', 22 | description: 'Remove all of the sandboxes before pulling', 23 | default: false, 24 | }), 25 | } 26 | 27 | async run() { 28 | const { flags } = this.parse(SandboxPull) 29 | 30 | await sandboxPull({ 31 | force: flags.force, 32 | refresh: flags.refresh, 33 | remove: flags.remove, 34 | config: this.config, 35 | userConfig: this.userConfig, 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'release-it' 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/command' 2 | export * from './lib/release/release' 3 | export * from './utils' 4 | export { ReleaseConfig } from './lib/release/interfaces/release-config' 5 | -------------------------------------------------------------------------------- /src/lib/config/config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@oclif/config' 2 | import { unlink } from 'fs-extra' 3 | import { get, has, set } from 'lodash' 4 | import { error, exec, log, warning } from '../../utils' 5 | import { UserConfig } from '../../utils/user-config' 6 | import { getConfigFile, getConfigFilePath, updateConfigFile } from './utils/config-utils' 7 | 8 | export interface EditConfigParamOptions { 9 | config: IConfig 10 | global: boolean 11 | userConfig: UserConfig 12 | } 13 | 14 | export interface GetConfigParamOptions extends EditConfigParamOptions { 15 | key: string 16 | } 17 | export interface SetConfigParamOptions extends GetConfigParamOptions { 18 | value: string 19 | } 20 | 21 | function validateOptions( 22 | options: EditConfigParamOptions | GetConfigParamOptions | SetConfigParamOptions, 23 | ) { 24 | if (!options.global) { 25 | error(`The cli currently only supports global variables.`) 26 | process.exit(1) 27 | } 28 | } 29 | 30 | export async function deleteConfig(options: EditConfigParamOptions) { 31 | validateOptions(options) 32 | const configFile = await getConfigFilePath(options.config) 33 | await unlink(configFile) 34 | log('DELETE', `Deleted ${configFile}`) 35 | } 36 | 37 | export async function editConfig(options: EditConfigParamOptions) { 38 | validateOptions(options) 39 | const configFile = await getConfigFilePath(options.config) 40 | const editor = process.env.EDITOR || 'vim' 41 | const command = `${editor} ${configFile}` 42 | exec(command) 43 | } 44 | 45 | export async function getConfigParam(options: GetConfigParamOptions) { 46 | validateOptions(options) 47 | const configFile = await getConfigFile(options.config) 48 | const keyExists = has(configFile, options.key) 49 | 50 | if (!keyExists) { 51 | warning(`Option ${options.key} is not set`) 52 | process.exit() 53 | } 54 | console.log(JSON.stringify(get(configFile, options.key), null, 2)) 55 | } 56 | 57 | export async function setConfigParam(options: SetConfigParamOptions) { 58 | validateOptions(options) 59 | const configFile = await getConfigFile(options.config) 60 | const keyExists = has(configFile, options.key) 61 | 62 | if (keyExists && get(configFile, options.key) === options.value) { 63 | warning(`Option ${options.key} is already set to ${options.value}`) 64 | process.exit() 65 | } 66 | const updated = set(configFile, options.key, options.value) 67 | await updateConfigFile(options.config, updated) 68 | log(keyExists ? 'UPDATE' : 'CREATE', `Set option ${options.key} to ${options.value}`) 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/config/utils/config-utils.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@oclif/config' 2 | import { readJSON, writeJSON } from 'fs-extra' 3 | import { join } from 'path' 4 | import { UserConfig } from '../../../utils/user-config' 5 | 6 | export function getConfigFilePath(config: IConfig) { 7 | return join(config.configDir, 'config.json') 8 | } 9 | export async function getConfigFile(config: IConfig) { 10 | return readJSON(getConfigFilePath(config)) 11 | } 12 | 13 | export async function updateConfigFile(config: IConfig, content: UserConfig) { 14 | return writeJSON(getConfigFilePath(config), content, { spaces: 2 }) 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/plugins/interfaces/plugin-config.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from '../../../utils/base-config' 2 | 3 | export interface PluginConfig extends BaseConfig { 4 | cwd: string 5 | refresh: boolean 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/plugins/plugins.ts: -------------------------------------------------------------------------------- 1 | import { readJSON } from 'fs-extra' 2 | import * as inquirer from 'inquirer' 3 | import { join } from 'path' 4 | import { 5 | error, 6 | exec, 7 | getWorkspaceInfo, 8 | gray, 9 | log, 10 | NX_COMMUNITY_PLUGINS_URL, 11 | NX_PLUGINS_URL, 12 | NXPM_PLUGINS_CACHE, 13 | NXPM_PLUGINS_URL, 14 | selectFromList, 15 | WorkspaceInfo, 16 | } from '../../utils' 17 | import { readAllSchematicCollections } from '../../utils/vendor/nx-console/read-schematic-collections' 18 | import { SchematicCollection } from '../../utils/vendor/nx-console/schema' 19 | import { BACK_OPTION, INSTALL_OPTION, REMOVE_OPTION } from '../projects/projects' 20 | import { PluginConfig } from './interfaces/plugin-config' 21 | import { pluginUrlCache } from './utils/plugin-utils' 22 | 23 | export interface NxPlugin { 24 | name: string 25 | description: string 26 | url: string 27 | } 28 | 29 | export const loadPluginsSchematics = async (info: WorkspaceInfo, config: PluginConfig) => { 30 | const cacheFile = join(config.config.cacheDir, NXPM_PLUGINS_CACHE) 31 | const pluginGroups = await readJSON(cacheFile) 32 | const plugins = Object.values(pluginGroups).flat() 33 | const schematics = await readAllSchematicCollections(info.workspaceJsonPath) 34 | const schematicsNames = schematics.map((s: SchematicCollection) => s.name) 35 | const availablePlugins = plugins.filter((p: NxPlugin) => !schematicsNames.includes(p.name)) 36 | const installedPlugins = plugins.filter((p: NxPlugin) => schematicsNames.includes(p.name)) 37 | const availablePluginNames = availablePlugins.map((p: NxPlugin) => p.name) 38 | const installedPluginNames = installedPlugins.map((p: NxPlugin) => p.name) 39 | return { 40 | plugins, 41 | schematics, 42 | schematicsNames, 43 | availablePlugins, 44 | availablePluginNames, 45 | installedPlugins, 46 | installedPluginNames, 47 | pluginNames: [...availablePluginNames, ...installedPluginNames], 48 | } 49 | } 50 | 51 | export const selectPlugin = async ( 52 | plugins: any[], 53 | message: string, 54 | ): Promise<{ pluginName: string } | false> => { 55 | const pluginName = await selectFromList(plugins, { message, addExit: true }) 56 | if (!pluginName) { 57 | return false 58 | } 59 | return { pluginName } 60 | } 61 | 62 | export const selectPluginFlow = async ( 63 | info: WorkspaceInfo, 64 | config: PluginConfig, 65 | pluginName?: string, 66 | ): Promise<{ selection: string; pluginName: string; plugin?: NxPlugin } | false> => { 67 | const { 68 | availablePluginNames, 69 | installedPluginNames, 70 | plugins, 71 | schematics, 72 | } = await loadPluginsSchematics(info, config) 73 | const options: any[] = [] 74 | 75 | if (!pluginName) { 76 | console.clear() 77 | if (availablePluginNames.length !== 0) { 78 | options.push(new inquirer.Separator('Available Plugins'), ...availablePluginNames.sort()) 79 | } 80 | if (installedPluginNames.length !== 0) { 81 | options.push(new inquirer.Separator('Installed Plugins'), ...installedPluginNames.sort()) 82 | } 83 | const pluginResult = await selectPlugin(options, 'Plugins') 84 | 85 | if (!pluginResult) { 86 | return Promise.resolve(false) 87 | } 88 | pluginName = pluginResult.pluginName 89 | } 90 | 91 | const plugin: any = plugins.find((p: NxPlugin) => p.name === pluginName) 92 | 93 | if (!plugin) { 94 | error(`Plugin ${pluginName} not found`) 95 | return Promise.resolve(false) 96 | } 97 | 98 | // eslint-disable-next-line no-console 99 | console.log(` 100 | ${plugin.description} 101 | ${gray(plugin.url)} 102 | `) 103 | const isInstalled = installedPluginNames.includes(pluginName) 104 | const availableOptions = [INSTALL_OPTION] 105 | const installedOptions = [REMOVE_OPTION] 106 | 107 | if (isInstalled) { 108 | const found = schematics.find((s: SchematicCollection) => s.name === pluginName) 109 | const schematicNames = found?.schematics.map((s) => s.name).reverse() 110 | schematicNames?.forEach((name: string) => installedOptions.unshift(`${pluginName}:${name}`)) 111 | } 112 | 113 | const selection = await selectFromList(isInstalled ? installedOptions : availableOptions, { 114 | addBack: true, 115 | addExit: true, 116 | message: pluginName, 117 | }) 118 | 119 | if (!selection) { 120 | return Promise.resolve(false) 121 | } 122 | 123 | return { 124 | selection, 125 | pluginName, 126 | plugin, 127 | } 128 | } 129 | 130 | const loop = async ( 131 | info: WorkspaceInfo, 132 | config: PluginConfig, 133 | { pluginName }: { pluginName?: string }, 134 | ) => { 135 | const result = await selectPluginFlow(info, config, pluginName) 136 | 137 | if (!result) { 138 | return 139 | } 140 | 141 | if (result.selection === INSTALL_OPTION) { 142 | const command = 143 | info.packageManager === 'yarn' 144 | ? `yarn add ${result.pluginName}` 145 | : `npm install ${result.pluginName}` 146 | log('Installing plugin') 147 | exec(command, { stdio: 'ignore' }) 148 | console.clear() 149 | await loop(info, config, { pluginName: result.pluginName }) 150 | } 151 | 152 | if (result.selection === REMOVE_OPTION) { 153 | const command = 154 | info.packageManager === 'yarn' 155 | ? `yarn remove ${result.pluginName}` 156 | : `npm uninstall ${result.pluginName}` 157 | log('Removing plugin') 158 | exec(command, { stdio: 'ignore' }) 159 | log('Done') 160 | } 161 | 162 | if (result.selection.startsWith(result.pluginName)) { 163 | log('Running schematic', result.selection) 164 | const command = `nx generate ${result.selection}` 165 | exec(command) 166 | log('Done') 167 | } 168 | 169 | if (result.selection === BACK_OPTION) { 170 | await loop(info, config, { pluginName }) 171 | } 172 | } 173 | 174 | export const plugins = async (config: PluginConfig): Promise => { 175 | const info = getWorkspaceInfo({ cwd: config.cwd }) 176 | await pluginUrlCache(config) 177 | await loop(info, config, {}) 178 | } 179 | -------------------------------------------------------------------------------- /src/lib/plugins/utils/plugin-utils.ts: -------------------------------------------------------------------------------- 1 | import { cli } from 'cli-ux' 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | import { 5 | cacheUrls, 6 | NX_COMMUNITY_PLUGINS_URL, 7 | NX_PLUGINS_URL, 8 | NXPM_PLUGINS_CACHE, 9 | NXPM_PLUGINS_URL, 10 | } from '../../../utils' 11 | import { PluginConfig } from '../interfaces/plugin-config' 12 | 13 | export async function pluginUrlCache(config: PluginConfig) { 14 | const cacheFile = join(config.config.cacheDir, NXPM_PLUGINS_CACHE) 15 | const urls = [NX_PLUGINS_URL, NX_COMMUNITY_PLUGINS_URL, NXPM_PLUGINS_URL] 16 | 17 | if (config.userConfig?.plugins?.urls) { 18 | urls.push(...config.userConfig?.plugins?.urls) 19 | } 20 | 21 | if (!existsSync(join(cacheFile)) || config.refresh) { 22 | cli.action.start(`Downloading plugins registry from ${urls.length} source(s)`) 23 | await cacheUrls(urls, cacheFile) 24 | cli.action.stop() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/projects/projects.ts: -------------------------------------------------------------------------------- 1 | import { readJSONSync } from 'fs-extra' 2 | import * as inquirer from 'inquirer' 3 | import { get } from 'lodash' 4 | import { join } from 'path' 5 | import { 6 | error, 7 | exec, 8 | getWorkspaceInfo, 9 | gray, 10 | log, 11 | selectFromList, 12 | WorkspaceInfo, 13 | } from '../../utils' 14 | import { BaseConfig } from '../../utils/base-config' 15 | 16 | export const BACK_OPTION = '[ BACK ]' 17 | export const EXIT_OPTION = '[ EXIT ]' 18 | export const INSTALL_OPTION = '[ INSTALL ]' 19 | export const REMOVE_OPTION = '[ REMOVE ]' 20 | export const RUN_OPTION = '[ RUN ]' 21 | export interface ProjectsConfig extends BaseConfig { 22 | cwd: string 23 | } 24 | 25 | export const setType = (type: string) => { 26 | switch (type) { 27 | case 'boolean': 28 | return 'confirm' 29 | default: 30 | return 'input' 31 | } 32 | } 33 | export const getSchematicParams = (cwd: string, param: string): Promise => { 34 | try { 35 | const [pkg, schematic] = param.split(':') 36 | const pkgRootPath = join(cwd, 'node_modules', pkg) 37 | 38 | // Get schematics property form {cwd}/node_modules/{pkg}/package.json 39 | const pkgJsonPath = join(pkgRootPath, 'package.json') 40 | const pkgJson = readJSONSync(pkgJsonPath) 41 | if (!pkgJson) { 42 | return Promise.reject(new Error(`Package ${pkg} not found`)) 43 | } 44 | if (!pkgJson.schematics) { 45 | return Promise.reject(new Error(`Package ${pkg} does not have schematics`)) 46 | } 47 | 48 | // Get collection.json defined in schematics 49 | const collectionPath = join(pkgRootPath, pkgJson.schematics) 50 | const collection = readJSONSync(collectionPath) 51 | if (!collection || !collection.schematics) { 52 | return Promise.reject(new Error(`Collections for ${pkg} not found`)) 53 | } 54 | if (!collection.schematics[schematic]) { 55 | return Promise.reject(new Error(`Collection in ${pkg} does not have schematic ${schematic}`)) 56 | } 57 | 58 | // Get schema.json from selected schematic 59 | const schematicDef = collection.schematics[schematic] 60 | if (!schematicDef.schema) { 61 | return Promise.resolve({}) 62 | } 63 | const schemaPath = join(pkgRootPath, schematicDef.schema) 64 | const schema = readJSONSync(schemaPath) 65 | 66 | if (!schema || !schema.properties) { 67 | return Promise.reject(new Error(`Properties for ${pkg}:${schematic} not found`)) 68 | } 69 | 70 | const schemaProperties = schema.properties 71 | const properties = [ 72 | ...Object.keys(schemaProperties).map((property) => ({ 73 | name: property, 74 | type: setType(schemaProperties[property].type), 75 | message: schemaProperties[property]?.description, 76 | default: schemaProperties[property]?.default, 77 | })), 78 | { 79 | name: 'dryRun', 80 | type: 'confirm', 81 | message: 'Do you want to do a dry-run?', 82 | default: false, 83 | }, 84 | ] 85 | 86 | return Promise.resolve({ properties }) 87 | } catch (error) { 88 | error(error) 89 | return Promise.reject(error) 90 | } 91 | } 92 | 93 | const selectProjectName = async (info: WorkspaceInfo): Promise => { 94 | const items: any = info.workspace?.projects || [] 95 | if (Object.keys(items).length === 0) { 96 | error("Can't find any projects in this workspace") 97 | return Promise.resolve(false) 98 | } 99 | 100 | const projectList = Object.keys(items).map((item: string) => ({ 101 | projectName: item, 102 | type: items[item].projectType, 103 | })) 104 | const apps: string[] = projectList 105 | .filter((t: any) => t.type === 'application') 106 | .map((t: any) => t.projectName) 107 | .sort() 108 | const libs: string[] = projectList 109 | .filter((t: any) => t.type === 'library') 110 | .map((t: any) => t.projectName) 111 | .sort() 112 | 113 | const options = [] 114 | if (apps.length !== 0) { 115 | options.push(new inquirer.Separator('Apps'), ...apps) 116 | } 117 | if (libs.length !== 0) { 118 | options.push(new inquirer.Separator('Libraries:'), ...libs) 119 | } 120 | const projectName = await selectFromList(options, { 121 | addExit: true, 122 | message: `Select project (${projectList.length} found)`, 123 | }) 124 | 125 | if (projectName === false) { 126 | return Promise.resolve(false) 127 | } 128 | 129 | return projectName 130 | } 131 | 132 | const selectProjectAction = async ( 133 | info: WorkspaceInfo, 134 | { 135 | target, 136 | params, 137 | projectName, 138 | project, 139 | }: { target?: string; params: { [key: string]: any }; projectName: string; project: any }, 140 | ): Promise<{ action: string; payload: any } | false> => { 141 | const answers: any = { projectName } 142 | const targets = Object.keys(project?.targets ?? project?.architect).sort() 143 | const schematics = ['@nrwl/workspace:move', '@nrwl/workspace:remove'] 144 | const projectOptions: any[] = [ 145 | new inquirer.Separator('Builders'), 146 | ...targets, 147 | new inquirer.Separator('Schematics'), 148 | ...schematics, 149 | ] 150 | if (!target) { 151 | const found = await selectFromList(projectOptions, { 152 | addExit: true, 153 | message: `Selected ${project.projectType} ${projectName} ${gray(project.root)}`, 154 | }) 155 | if (!found) { 156 | error(`Action not found`) 157 | return Promise.resolve(false) 158 | } 159 | target = found 160 | } 161 | 162 | if (targets.includes(target)) { 163 | const architectParams = get(params, `${target}.params`, '') 164 | return { 165 | action: 'exec', 166 | payload: `nx run ${projectName}:${target} ${architectParams}`, 167 | } 168 | } 169 | if (schematics.includes(target)) { 170 | const params = await getSchematicParams(info.cwd, target) 171 | const payload = [`nx generate ${target}`] 172 | if (Object.keys(params.properties).length !== 0) { 173 | Object.keys(answers).forEach((answer) => payload.push(` --${answer} ${answers[answer]}`)) 174 | 175 | const res: any = await inquirer.prompt( 176 | params.properties.filter((p: any) => { 177 | return !Object.keys(answers).includes(p.name) 178 | }), 179 | ) 180 | Object.keys(res).forEach((answer) => payload.push(` --${answer} ${res[answer]}`)) 181 | } 182 | 183 | return { action: 'exec', payload: payload.join(' ') } 184 | } 185 | return Promise.resolve(false) 186 | } 187 | 188 | export const interactive = async ( 189 | info: WorkspaceInfo, 190 | config: ProjectsConfig, 191 | { projectName, target }: { projectName?: string; target?: string }, 192 | ) => { 193 | if (!projectName) { 194 | const res = await selectProjectName(info) 195 | if (res) { 196 | projectName = res 197 | } 198 | } 199 | 200 | if (typeof projectName === 'undefined') { 201 | return 202 | } 203 | 204 | const project = info?.workspace?.projects[projectName] 205 | 206 | if (!project) { 207 | error(`Project ${projectName} not found`) 208 | return 209 | } 210 | 211 | const params = get(config?.userConfig, 'projects', {}) 212 | const projectActionResult = await selectProjectAction(info, { 213 | target, 214 | projectName, 215 | project, 216 | params, 217 | }) 218 | 219 | if (projectActionResult === false) { 220 | return 221 | } 222 | 223 | if (projectActionResult.action === 'exec') { 224 | exec(`${projectActionResult.payload}`) 225 | exec(`yarn format`, { stdio: 'ignore' }) 226 | } else { 227 | error(`Unknown action ${projectActionResult.action}`) 228 | } 229 | } 230 | export const projects = async ( 231 | config: ProjectsConfig, 232 | projectName?: string, 233 | target?: string, 234 | ): Promise => { 235 | log('Projects', gray(`Working directory ${config.cwd}`)) 236 | const info = getWorkspaceInfo({ cwd: config.cwd }) 237 | 238 | await interactive(info, config, { projectName, target }) 239 | } 240 | -------------------------------------------------------------------------------- /src/lib/release/interfaces/release-config.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from '../../../utils/base-config' 2 | 3 | export interface ReleaseConfig extends BaseConfig { 4 | build: boolean 5 | ci: boolean 6 | cwd: string 7 | dryRun: boolean 8 | fix: boolean 9 | local?: boolean 10 | localUrl?: string 11 | version: string 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/release/interfaces/validated-config.ts: -------------------------------------------------------------------------------- 1 | import { ReleaseConfig } from './release-config' 2 | 3 | export interface ValidatedConfig extends ReleaseConfig { 4 | // Trigger the build 5 | build: boolean 6 | // NPM Scope defined in nx.json 7 | npmScope: string 8 | // Tag we aim to publish 9 | npmTag: 'latest' | 'next' 10 | // Content of the nx.json file 11 | nx: { [key: string]: any } 12 | // Content of the package.json file 13 | package: { [key: string]: any } 14 | // Whether it's a pre release or not 15 | preRelease: boolean 16 | // Content of angular.json or workspace.json 17 | workspace: { [key: string]: any } 18 | // Path to angular.json or workspace.json 19 | workspacePath: string 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/release/interfaces/validated-packages.ts: -------------------------------------------------------------------------------- 1 | export interface ValidatedPackages { 2 | pkgFiles: string[] 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/release/interfaces/validated-workspace.ts: -------------------------------------------------------------------------------- 1 | export interface ValidatedWorkspace { 2 | packages: any[] 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/release/release-validate.ts: -------------------------------------------------------------------------------- 1 | import { getProjects } from '@nrwl/devkit' 2 | import { FsTree } from '@nrwl/tao/src/shared/tree' 3 | import { existsSync, writeFileSync } from 'fs' 4 | import { readJSONSync } from 'fs-extra' 5 | import { join, relative, resolve } from 'path' 6 | import { 7 | error, 8 | exec, 9 | getWorkspaceInfo, 10 | gray, 11 | log, 12 | NX_PACKAGE_BUILDERS, 13 | parseVersion, 14 | red, 15 | validatePackageJson, 16 | yellowBright, 17 | } from '../../utils' 18 | 19 | import { ReleaseConfig } from './interfaces/release-config' 20 | import { ValidatedConfig } from './interfaces/validated-config' 21 | import { ValidatedPackages } from './interfaces/validated-packages' 22 | import { ValidatedWorkspace } from './interfaces/validated-workspace' 23 | 24 | export function validateConfig(config: ReleaseConfig): ValidatedConfig | false { 25 | const info = getWorkspaceInfo({ cwd: config.cwd }) 26 | const { isPrerelease } = parseVersion(config.version) 27 | const validated: ValidatedConfig = { 28 | ...config, 29 | nx: info.nx, 30 | npmScope: `@${info.nx.npmScope}`, 31 | npmTag: isPrerelease ? 'next' : 'latest', 32 | package: info.package, 33 | preRelease: isPrerelease, 34 | workspacePath: info.path, 35 | workspace: info.workspace, 36 | } 37 | 38 | if (!validated.version) { 39 | log(red('ERROR'), 'Please provide the release version (like: 1.2.3-beta.4)') 40 | return false 41 | } 42 | 43 | if (!parseVersion(config.version).isValid) { 44 | log(red('ERROR'), 'Please provide a valid release version (like: 1.2.3-beta.4)') 45 | return false 46 | } 47 | 48 | log( 49 | 'VALIDATE', `Using Nx workspace: ${gray(relative(config.cwd, validated.workspacePath))}`, 50 | ) 51 | return validated 52 | } 53 | 54 | export function validateWorkspace(config: ValidatedConfig): ValidatedWorkspace | false { 55 | const host = new FsTree(process.cwd(), true) 56 | const projects = getProjects(host) 57 | 58 | const libs = [...projects.keys()] 59 | .map((id) => ({ id, ...projects.get(id) })) 60 | .filter((project) => project.projectType === 'library') 61 | 62 | if (!libs.length) { 63 | throw new Error(`No libraries found in nx workspace ${config.workspacePath}`) 64 | } 65 | 66 | log('VALIDATE', `Found ${yellowBright(libs.length)} libraries:`) 67 | 68 | // Find libraries that have a package executor 69 | const packages = libs 70 | .map((lib) => ({ 71 | lib, 72 | target: Object.keys(lib.targets) 73 | .map((id) => ({ id, ...lib.targets[id] })) 74 | // We only include targets that are called 'build' 75 | .filter((target) => target.id === 'build') 76 | .find(({ executor }) => NX_PACKAGE_BUILDERS.includes(executor)), 77 | })) 78 | // Only release packages which turned out to have at least one 'publishable' target 79 | .filter((lib) => !!lib.target) 80 | 81 | for (const pkg of packages) { 82 | log('VALIDATE', `Found executor for ${yellowBright(pkg.lib.id)}: ${gray(pkg.target.executor)} `) 83 | } 84 | 85 | return { 86 | packages, 87 | } 88 | } 89 | 90 | export function validatePackages( 91 | config: ValidatedConfig, 92 | workspace: ValidatedWorkspace, 93 | ): ValidatedPackages | false { 94 | // Validate libraries before packaging 95 | const invalid = workspace.packages.filter(({ lib }) => { 96 | const name = `${config.npmScope}/${lib.id}` 97 | return !validatePackageJson(lib.root, { 98 | dryRun: config.dryRun, 99 | fix: config.fix, 100 | version: config.version, 101 | name, 102 | workspacePkgJson: config.package, 103 | }) 104 | }) 105 | 106 | if (invalid.length) { 107 | const invalidIds = invalid.map((item) => item.lib.id) 108 | log(red('Could not continue because of errors in the following packages:')) 109 | console.log(invalidIds) 110 | if (!config.fix) { 111 | log('Try running this command with the --fix flag to fix some common problems') 112 | } 113 | return false 114 | } 115 | 116 | const pkgFiles: string[] = workspace.packages 117 | .map((pkg) => { 118 | if (!pkg.target?.options?.outputPath && !pkg.target?.options?.project) { 119 | console.log('pkg.target?.options', pkg.target?.options) 120 | throw new Error(`Error determining dist path for ${pkg.lib.id}`) 121 | } 122 | 123 | if (pkg.target?.options?.outputPath) { 124 | // @nrwl/node:package builder 125 | return pkg.target?.options?.outputPath 126 | } 127 | if (pkg?.target?.options?.project) { 128 | // @nrwl/angular:package builder 129 | const ngPackagePath = join(config.cwd, pkg?.target?.options?.project) 130 | const ngPackageJson = readJSONSync(ngPackagePath) 131 | return relative(config.cwd, resolve(pkg.lib.root, ngPackageJson.dest)) 132 | } 133 | throw new Error("Can't find pkg file") 134 | }) 135 | .map((file) => join(file, 'package.json')) 136 | 137 | if (config.build) { 138 | exec('yarn nx run-many --target build --all') 139 | } 140 | 141 | // Here we check of the expected packages are built 142 | const foundPkgFiles = pkgFiles.map((pkgFile) => { 143 | const pkgPath = join(config.cwd, pkgFile) 144 | const exists = existsSync(pkgPath) 145 | if (!exists) { 146 | error(`Could not find ${pkgFile}. Make sure to build your packages before releasing`) 147 | return false 148 | } 149 | 150 | const pkgJson = readJSONSync(pkgPath) 151 | writeFileSync( 152 | pkgPath, 153 | JSON.stringify( 154 | { 155 | ...pkgJson, 156 | version: config.version, 157 | }, 158 | null, 159 | 2, 160 | ), 161 | ) 162 | 163 | return pkgFile 164 | }) 165 | 166 | if (foundPkgFiles.length !== pkgFiles.length) { 167 | return false 168 | } 169 | 170 | if (!foundPkgFiles.length) { 171 | error('VALIDATE', 'Found no packages to release') 172 | return false 173 | } 174 | 175 | log('VALIDATE', `Found ${foundPkgFiles.length} packages to release`) 176 | 177 | return { 178 | pkgFiles, 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/lib/release/release.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { error, exec, log, runNpmPublish, runReleaseIt } from '../../utils' 3 | import { ReleaseConfig } from './interfaces/release-config' 4 | import { validateConfig, validatePackages, validateWorkspace } from './release-validate' 5 | 6 | export const release = async (_config: ReleaseConfig): Promise => { 7 | // Validate the config and read required files 8 | const config = validateConfig(_config) 9 | 10 | if (config === false) { 11 | error('Error validating configuration') 12 | process.exit(1) 13 | } 14 | 15 | const workspace = validateWorkspace(config) 16 | 17 | if (workspace === false) { 18 | error('Error validating workspace') 19 | process.exit(1) 20 | } 21 | 22 | const packages = validatePackages(config, workspace) 23 | 24 | if (packages === false) { 25 | error('Error validating packages') 26 | process.exit(1) 27 | } 28 | 29 | log('RUN', 'Fetching git info release') 30 | exec('git fetch --all', { stdio: 'pipe' }) 31 | 32 | if (config.local) { 33 | log('DRY-RUN', 'Skipping GitHub release') 34 | } else { 35 | const releaseResult = await runReleaseIt({ 36 | dryRun: config.dryRun, 37 | pkgFiles: [join(config.cwd, 'package.json'), ...packages.pkgFiles], 38 | preRelease: config.preRelease, 39 | version: config.version, 40 | ci: config.ci, 41 | }) 42 | 43 | if (!releaseResult) { 44 | error("Something went wrong running 'release-it' :( ") 45 | process.exit(1) 46 | } 47 | } 48 | 49 | if (config.dryRun) { 50 | log('DRY-RUN', 'Skipping npm publish') 51 | } else { 52 | const publishResult = runNpmPublish({ 53 | dryRun: config.dryRun, 54 | local: config.local, 55 | localUrl: config.localUrl, 56 | pkgFiles: packages.pkgFiles, 57 | version: config.version, 58 | tag: config.npmTag, 59 | }) 60 | 61 | if (!publishResult) { 62 | error("Something went wrong running 'npm publish' :( ") 63 | process.exit(1) 64 | } 65 | 66 | if (publishResult) { 67 | log('SUCCESS', "It looks like we're all done here! :)") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/sandbox/interfaces/sandbox-config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@oclif/config' 2 | import { BaseConfig } from '../../../utils/base-config' 3 | 4 | export interface SandboxConfig extends BaseConfig { 5 | action?: string 6 | config: IConfig 7 | portApi?: string 8 | portWeb?: string 9 | ports?: string 10 | refresh: boolean 11 | sandboxId?: string 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/sandbox/interfaces/sandbox-pull-config.ts: -------------------------------------------------------------------------------- 1 | import { SandboxConfig } from './sandbox-config' 2 | 3 | export interface SandboxPullConfig extends SandboxConfig { 4 | force: boolean 5 | remove: boolean 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/sandbox/interfaces/sandbox.ts: -------------------------------------------------------------------------------- 1 | export interface Sandbox { 2 | id: string 3 | name: string 4 | description: string 5 | url: string 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/sandbox/sandbox-pull.ts: -------------------------------------------------------------------------------- 1 | import { cli } from 'cli-ux' 2 | import { error, log } from '../../utils' 3 | import { SandboxPullConfig } from './interfaces/sandbox-pull-config' 4 | import { 5 | getDockerImages, 6 | getSandboxUrlCache, 7 | pullDockerImage, 8 | removeDockerImage, 9 | sandboxUrlCache, 10 | } from './utils/sandbox-utils' 11 | 12 | const pull = async (config: SandboxPullConfig) => { 13 | const existing = await getDockerImages() 14 | const sandboxes = await getSandboxUrlCache(config) 15 | 16 | const filtered = config.remove ? sandboxes : sandboxes.filter((s) => !existing.includes(s.name)) 17 | 18 | if (config.remove) { 19 | cli.action.start(`Remove ${filtered.length} Docker images ${config.force ? '(FORCED)' : null}`) 20 | for (let sandbox of filtered) { 21 | cli.action.status = sandbox.name 22 | try { 23 | await removeDockerImage(sandbox.name, config.force) 24 | } catch (e) {} 25 | } 26 | cli.action.stop() 27 | } 28 | 29 | if (filtered.length) { 30 | cli.action.start(`Pulling ${filtered.length} Docker images`) 31 | for (let sandbox of filtered) { 32 | cli.action.status = sandbox.name 33 | try { 34 | await pullDockerImage(sandbox.name) 35 | } catch (e) { 36 | error(e.message) 37 | } 38 | } 39 | cli.action.stop() 40 | } else { 41 | log('SANDBOX:PULL', 'Nothing to pull, you are all in sync!') 42 | } 43 | } 44 | 45 | export const sandboxPull = async (config: SandboxPullConfig): Promise => { 46 | await sandboxUrlCache(config) 47 | await pull(config) 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/sandbox/sandbox.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from 'inquirer' 2 | import { error, gray, log, selectFromList } from '../../utils' 3 | import { BACK_OPTION, INSTALL_OPTION, REMOVE_OPTION, RUN_OPTION } from '../projects/projects' 4 | import { Sandbox } from './interfaces/sandbox' 5 | import { SandboxConfig } from './interfaces/sandbox-config' 6 | import { 7 | getDockerImages, 8 | getSandboxUrlCache, 9 | pullDockerImage, 10 | removeDockerImage, 11 | runDockerImage, 12 | sandboxUrlCache, 13 | } from './utils/sandbox-utils' 14 | 15 | export interface NxPlugin { 16 | name: string 17 | description: string 18 | url: string 19 | } 20 | 21 | export const selectSandbox = async ( 22 | sandboxes: any[], 23 | message: string, 24 | ): Promise<{ sandboxName: string } | false> => { 25 | const sandboxName = await selectFromList(sandboxes, { message, addExit: true }) 26 | if (!sandboxName) { 27 | return false 28 | } 29 | return { sandboxName } 30 | } 31 | export const getConfigAction = (config: SandboxConfig) => { 32 | switch (config.action) { 33 | case 'run': 34 | return RUN_OPTION 35 | } 36 | } 37 | 38 | export const selectSandboxFlow = async ( 39 | config: SandboxConfig, 40 | sandboxName?: string, 41 | ): Promise<{ selection: string; sandboxName: string; sandbox?: Sandbox } | false> => { 42 | const [sandboxes, images] = await Promise.all([getSandboxUrlCache(config), getDockerImages()]) 43 | if (config.sandboxId) { 44 | sandboxName = sandboxes.find((sandbox) => sandbox.id === config.sandboxId)?.name 45 | } 46 | const availableSandboxes: any[] = sandboxes 47 | .map((s) => s.name) 48 | .filter((name) => !images.includes(name)) 49 | const installedSandboxes: any[] = sandboxes 50 | .map((s) => s.name) 51 | .filter((name) => images.includes(name)) 52 | 53 | const options: any[] = [] 54 | if (!sandboxName) { 55 | console.clear() 56 | if (installedSandboxes.length !== 0) { 57 | options.push(new inquirer.Separator('Installed Sandboxes'), ...installedSandboxes.sort()) 58 | } 59 | if (availableSandboxes.length !== 0) { 60 | options.push(new inquirer.Separator('Available Sandboxes'), ...availableSandboxes.sort()) 61 | } 62 | const sandboxResult = await selectSandbox(options, 'Sandboxes') 63 | 64 | if (!sandboxResult) { 65 | return Promise.resolve(false) 66 | } 67 | sandboxName = sandboxResult.sandboxName 68 | } 69 | 70 | const sandbox = sandboxes.find((p: NxPlugin) => p.name === sandboxName) 71 | 72 | if (!sandbox) { 73 | error(`Plugin ${sandboxName} not found`) 74 | return Promise.resolve(false) 75 | } 76 | 77 | // eslint-disable-next-line no-console 78 | console.log(` 79 | ${sandbox.description} 80 | ${gray(sandbox.url)} 81 | `) 82 | const isInstalled = installedSandboxes.includes(sandboxName) 83 | const availableOptions = [INSTALL_OPTION] 84 | const installedOptions = [RUN_OPTION, REMOVE_OPTION] 85 | 86 | const selection = config.action 87 | ? getConfigAction(config) 88 | : await selectFromList(isInstalled ? installedOptions : availableOptions, { 89 | addBack: true, 90 | addExit: true, 91 | message: sandboxName, 92 | }) 93 | 94 | if (!selection) { 95 | return Promise.resolve(false) 96 | } 97 | 98 | return { 99 | selection, 100 | sandboxName, 101 | sandbox, 102 | } 103 | } 104 | 105 | const loop = async (config: SandboxConfig, { sandboxName }: { sandboxName?: string }) => { 106 | const result = await selectSandboxFlow(config, sandboxName) 107 | 108 | if (!result) { 109 | return 110 | } 111 | 112 | if (result.selection === INSTALL_OPTION) { 113 | log('INSTALL', result.sandboxName) 114 | await pullDockerImage(result.sandboxName) 115 | console.clear() 116 | await loop(config, { sandboxName: result.sandboxName }) 117 | } 118 | 119 | if (result.selection === REMOVE_OPTION) { 120 | log('REMOVE', result.sandboxName) 121 | try { 122 | await removeDockerImage(result.sandboxName, false) 123 | } catch (e) { 124 | error(e.message) 125 | const res = await inquirer.prompt([ 126 | { 127 | name: 'force', 128 | type: 'confirm', 129 | message: 'Do you want to force removal?', 130 | default: false, 131 | }, 132 | ]) 133 | if (res.force) { 134 | await removeDockerImage(result.sandboxName, true) 135 | } 136 | } 137 | await loop(config, { sandboxName: undefined }) 138 | } 139 | 140 | if (result.selection === RUN_OPTION && result.sandbox) { 141 | log('RUN', `${result.sandbox.id} ${gray(result.sandboxName)}`) 142 | try { 143 | const ports: string[] = [] 144 | if (config.portApi) { 145 | ports.push(config.portApi) 146 | } 147 | if (config.portWeb) { 148 | ports.push(config.portWeb) 149 | } 150 | if (config.ports) { 151 | ports.push(...(config.ports.includes(',') ? config.ports.split(',') : [config.ports])) 152 | } 153 | await runDockerImage(result.sandboxName, { 154 | options: { 155 | hostname: result.sandbox.id, 156 | name: result.sandbox.id, 157 | params: [], 158 | ports, 159 | }, 160 | }) 161 | } catch (e) { 162 | error(e.message) 163 | } 164 | // await loop(config, { sandboxName: result.sandboxName }) 165 | } 166 | 167 | if (result.selection.startsWith(result.sandboxName)) { 168 | log('Running sandbox', result.selection) 169 | // const command = `nx generate ${result.selection}` 170 | // exec(command) 171 | log('Done') 172 | } 173 | 174 | if (result.selection === BACK_OPTION) { 175 | await loop(config, {}) 176 | } 177 | } 178 | 179 | export const sandbox = async (config: SandboxConfig): Promise => { 180 | await sandboxUrlCache(config) 181 | await loop(config, {}) 182 | } 183 | -------------------------------------------------------------------------------- /src/lib/sandbox/utils/sandbox-utils.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process' 2 | import { cli } from 'cli-ux' 3 | import { existsSync } from 'fs' 4 | import { readJSON } from 'fs-extra' 5 | import { join } from 'path' 6 | import { 7 | cacheUrls, 8 | error, 9 | exec, 10 | gray, 11 | log, 12 | NXPM_SANDBOX_CACHE, 13 | NXPM_SANDBOXES_URL, 14 | } from '../../../utils' 15 | import { Sandbox } from '../interfaces/sandbox' 16 | import { SandboxConfig } from '../interfaces/sandbox-config' 17 | 18 | export async function getDockerContainer(name: string) { 19 | const raw = spawnSync(`docker`, [ 20 | 'ps', 21 | '--no-trunc', 22 | '-f', 23 | `name=${name}`, 24 | `--no-trunc`, 25 | `--format`, 26 | `{{ json . }}`, 27 | ]) 28 | if (raw.stdout) { 29 | try { 30 | return JSON.parse(raw.stdout.toString()) 31 | } catch (e) {} 32 | } 33 | } 34 | 35 | export async function getDockerImages() { 36 | const res: any[] = [] 37 | const raw = spawnSync(`docker`, ['images', `--no-trunc`, `--format`, `{{ json . }}`]) 38 | 39 | if (raw.stdout) { 40 | const lines = raw.stdout?.toString() 41 | lines 42 | ?.split('\n') 43 | .filter((line) => !!line) 44 | .forEach((line) => { 45 | try { 46 | const parsed = JSON.parse(line) 47 | if (parsed.Repository && parsed.Tag) { 48 | res.push(`${parsed.Repository}:${parsed.Tag}`) 49 | } 50 | } catch (e) {} 51 | }) 52 | } 53 | return res 54 | } 55 | 56 | export async function sandboxUrlCache(config: SandboxConfig) { 57 | const cacheFile = join(config.config.cacheDir, NXPM_SANDBOX_CACHE) 58 | const urls = [NXPM_SANDBOXES_URL] 59 | 60 | if (config.userConfig?.sandbox?.urls) { 61 | urls.push(...config.userConfig?.sandbox?.urls) 62 | } 63 | 64 | if (!existsSync(join(cacheFile)) || config.refresh) { 65 | cli.action.start(`Downloading sandbox registry from ${urls.length} source(s)`) 66 | await cacheUrls(urls, cacheFile) 67 | cli.action.stop() 68 | } 69 | } 70 | 71 | export async function getSandboxUrlCache(config: SandboxConfig): Promise { 72 | const cacheFile = join(config.config.cacheDir, NXPM_SANDBOX_CACHE) 73 | const sandboxGroups = await readJSON(cacheFile) 74 | return Object.values(sandboxGroups).flat() as Sandbox[] 75 | } 76 | 77 | export async function removeDockerImage(image: string, force: boolean) { 78 | return exec(`docker rmi ${force ? '-f' : ''} ${image}`, { stdio: [] }) 79 | } 80 | 81 | export async function pullDockerImage(image: string) { 82 | return exec(`docker pull ${image}`) 83 | } 84 | 85 | export async function attachDockerImage(image: string, options: { name: string }) { 86 | const existing = await getDockerContainer(options.name) 87 | 88 | if (!existing) { 89 | error(`Can't find container ${options.name}`) 90 | return Promise.reject() 91 | } 92 | 93 | const cmd = 'docker' 94 | const action = 'exec' 95 | const params = ['-it'] 96 | const command = [cmd, action, params.join(' '), options.name, 'zsh', '&& true'].join(' ') 97 | log('ATTACH', gray(command)) 98 | return exec(command) 99 | } 100 | 101 | export async function runDockerImage( 102 | image: string, 103 | { options }: { options: { hostname?: string; name: string; params?: string[]; ports: string[] } }, 104 | ) { 105 | const existing = await getDockerContainer(options.name) 106 | 107 | if (existing) { 108 | return attachDockerImage(image, { name: options.name }) 109 | } 110 | 111 | const host = process.env.DOCKER_MACHINE_NAME ? process.env.DOCKER_MACHINE_NAME : 'localhost' 112 | const cmd = 'docker' 113 | const action = 'run' 114 | const ports = options.ports 115 | .map((p) => (p.includes(':') ? p : `${p}:${p}`)) 116 | .map((p) => { 117 | log('LISTEN', `http://${host}:${p.split(':')[0]}`) 118 | return p 119 | }) 120 | .map((p) => `-p ${p}`) 121 | .join(' ') 122 | 123 | const defaultParams = [ 124 | '-it', 125 | '--rm', 126 | options.name ? `--name ${options.name}` : '', 127 | options.hostname ? `--hostname ${options.hostname}` : '', 128 | ports, 129 | ] 130 | const params = options.params || [] 131 | const command = [cmd, action, defaultParams.join(' '), params.join(' '), image, '&& true'].join( 132 | ' ', 133 | ) 134 | log('RUN', gray(command)) 135 | return exec(command) 136 | } 137 | -------------------------------------------------------------------------------- /src/lib/verdaccio/index.ts: -------------------------------------------------------------------------------- 1 | import { run } from '../../utils' 2 | 3 | export function startRegistry() { 4 | run('npx verdaccio') 5 | } 6 | 7 | export function disableRegistry() { 8 | run('npm config delete registry') 9 | run('yarn config delete registry') 10 | } 11 | 12 | export function enableRegistry() { 13 | run('npm config set registry http://localhost:4873/') 14 | run('yarn config set registry http://localhost:4873/') 15 | } 16 | 17 | export function registryStatus() { 18 | run(`npm config get registry`) 19 | run(`yarn config get registry`) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/base-command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { mkdirp, pathExists, readJSON } from 'fs-extra' 3 | import { join } from 'path' 4 | import { updateConfigFile } from '../lib/config/utils/config-utils' 5 | import { defaultUserConfig, UserConfig } from './user-config' 6 | 7 | export abstract class BaseCommand extends Command { 8 | public readonly configFile = join(this.config.configDir, 'config.json') 9 | 10 | public userConfig: UserConfig = defaultUserConfig 11 | 12 | async init() { 13 | if (!(await pathExists(this.configFile))) { 14 | if (!(await pathExists(this.config.configDir))) { 15 | await mkdirp(this.config.configDir) 16 | } 17 | await updateConfigFile(this.config, defaultUserConfig) 18 | } 19 | this.userConfig = await readJSON(this.configFile) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/base-config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@oclif/config' 2 | import { UserConfig } from './user-config' 3 | 4 | export interface BaseConfig { 5 | config: IConfig 6 | cwd?: string 7 | dryRun?: boolean 8 | userConfig?: UserConfig 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // These are the builders that the CLI knows can produce 'publishable' libs 2 | export const NX_PACKAGE_BUILDERS = [ 3 | '@nrwl/angular:package', 4 | '@nrwl/nest:package', 5 | '@nrwl/node:package', 6 | '@nrwl/workspace:run-commands', 7 | 'nx:run-commands', 8 | '@nrwl/web:package', 9 | '@nrwl/web:rollup', 10 | '@nrwl/rollup:rollup', 11 | '@nrwl/js:tsc', 12 | '@nrwl/js:swc', 13 | ] 14 | export const NXPM_PLUGINS_CACHE = 'nxpm-plugins.json' 15 | export const NXPM_SANDBOX_CACHE = 'nxpm-sandbox.json' 16 | export const NX_PLUGINS_URL = 17 | 'https://gist.githubusercontent.com/beeman/11c2761fc1b6681182af3271b2badcaa/raw/official-plugins.json' 18 | export const NX_COMMUNITY_PLUGINS_URL = 19 | 'https://raw.githubusercontent.com/nrwl/nx/master/community/approved-plugins.json' 20 | export const NXPM_PLUGINS_URL = 21 | 'https://gist.githubusercontent.com/beeman/11c2761fc1b6681182af3271b2badcaa/raw/nxpm-plugins.json' 22 | export const NXPM_SANDBOXES_URL = 23 | 'https://raw.githubusercontent.com/nxpm/nxpm-docker-images/master/nxpm-sandboxes.json' 24 | -------------------------------------------------------------------------------- /src/utils/get-workspace-info.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { readJSONSync } from 'fs-extra' 3 | import { join } from 'path' 4 | import { log } from './logging' 5 | 6 | export interface WorkspaceInfo { 7 | cwd: string 8 | package: { [key: string]: any } 9 | nx: { [key: string]: any } 10 | packageManager: 'npm' | 'yarn' 11 | path: string 12 | workspace: { [key: string]: any } 13 | workspaceJsonPath: string 14 | } 15 | 16 | export interface WorkspaceParams { 17 | cwd: string 18 | } 19 | 20 | export function getWorkspaceInfo({ cwd }: WorkspaceParams): WorkspaceInfo { 21 | const nxJsonPath = join(cwd, 'nx.json') 22 | const packageJsonPath = join(cwd, 'package.json') 23 | const packageLockJsonPath = join(cwd, 'package-lock.json') 24 | const yarnLockPath = join(cwd, 'yarn.lock') 25 | 26 | const packageLockJsonExists = existsSync(packageLockJsonPath) 27 | const yarnLockExists = existsSync(yarnLockPath) 28 | 29 | if (packageLockJsonExists && yarnLockExists) { 30 | log('WARNING', 'Found package-lock.json AND yarn.lock - defaulting to yarn.') 31 | } 32 | 33 | const workspacePath = join(process.cwd(), 'nx.json') 34 | if (!existsSync(nxJsonPath)) { 35 | throw new Error(`Can't find nx.json in ${nxJsonPath}`) 36 | } 37 | 38 | const packageManager = yarnLockExists ? 'yarn' : 'npm' 39 | 40 | return { 41 | cwd, 42 | package: readJSONSync(packageJsonPath), 43 | nx: existsSync(nxJsonPath) ? readJSONSync(nxJsonPath) : {}, 44 | path: workspacePath, 45 | workspace: readJSONSync(workspacePath), 46 | workspaceJsonPath: workspacePath, 47 | packageManager, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-command' 2 | export * from './constants' 3 | export * from './logging' 4 | export * from './utils' 5 | export * from './get-workspace-info' 6 | export * from './parse-version' 7 | -------------------------------------------------------------------------------- /src/utils/logging.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk' 2 | 3 | export const { 4 | whiteBright, 5 | greenBright, 6 | bold, 7 | inverse, 8 | redBright, 9 | red, 10 | yellowBright, 11 | magentaBright, 12 | gray, 13 | } = chalk 14 | 15 | const info = (label = 'NXPM'): string => inverse(magentaBright(bold(` ${label} `))) 16 | const err = (label = 'ERROR'): string => inverse(redBright(bold(` ${label} `))) 17 | const warn = (label = 'WARN'): string => inverse(yellowBright(bold(` ${label} `))) 18 | export const log = (msg: string, ...params: unknown[]): void => 19 | console.log(`${info()}`, `${greenBright(msg)}`, ...params) 20 | 21 | export const error = (msg: string, ...params: unknown[]): void => 22 | log(`${err()}`, `${redBright(msg)}`, ...params) 23 | 24 | export const warning = (msg: string, ...params: unknown[]): void => 25 | log(`${warn()}`, `${yellowBright(msg)}`, ...params) 26 | -------------------------------------------------------------------------------- /src/utils/parse-version.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedVersion { 2 | version: string 3 | isValid: boolean 4 | isPrerelease: boolean 5 | } 6 | 7 | export const parseVersion = (version: string): ParsedVersion => { 8 | if (!version || !version.length) { 9 | return { 10 | version, 11 | isValid: false, 12 | isPrerelease: false, 13 | } 14 | } 15 | const sections = version.split('-') 16 | if (sections.length === 1) { 17 | /** 18 | * Not a prerelease version, validate matches exactly the 19 | * standard {number}.{number}.{number} format 20 | */ 21 | return { 22 | version, 23 | isValid: !!sections[0].match(/\d+\.\d+\.\d+$/), 24 | isPrerelease: false, 25 | } 26 | } 27 | /** 28 | * Is a prerelease version, validate each section 29 | * 1. {number}.{number}.{number} format 30 | * 2. {alpha|beta|rc}.{number} 31 | */ 32 | return { 33 | version, 34 | isValid: !!(sections[0].match(/\d+\.\d+\.\d+$/) && sections[1].match(/(alpha|beta|rc)\.\d+$/)), 35 | isPrerelease: true, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/user-config.ts: -------------------------------------------------------------------------------- 1 | export interface UserConfig { 2 | plugins?: { 3 | urls?: string[] 4 | } 5 | release?: { 6 | github?: { 7 | token?: string | null 8 | } 9 | } 10 | projects?: { 11 | serve?: { 12 | params?: string 13 | } 14 | } 15 | sandbox?: { 16 | urls?: string[] 17 | } 18 | } 19 | 20 | export const defaultUserConfig: UserConfig = { 21 | plugins: { 22 | urls: [], 23 | }, 24 | release: { 25 | github: { 26 | token: null, 27 | }, 28 | }, 29 | sandbox: { 30 | urls: [], 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core' 2 | import { execSync, ExecSyncOptions } from 'child_process' 3 | import { existsSync } from 'fs' 4 | import { mkdirpSync, readJSONSync, writeFileSync, writeJSONSync } from 'fs-extra' 5 | import * as inquirer from 'inquirer' 6 | import fetch from 'node-fetch' 7 | import { dirname, join, relative } from 'path' 8 | 9 | import * as releaseIt from 'release-it' 10 | import { BACK_OPTION, EXIT_OPTION } from '../lib/projects/projects' 11 | 12 | import { gray, greenBright, log, red, yellowBright } from './logging' 13 | 14 | export const exec = (command: string, options?: ExecSyncOptions): Buffer => 15 | execSync(command, { stdio: [0, 1, 2], ...options }) 16 | 17 | export const run = (command: string) => { 18 | log('RUNNING', command) 19 | exec(command) 20 | } 21 | 22 | export const getPackageJson = (root: string): { [key: string]: any } | null => { 23 | const pkgPath = join(process.cwd(), root, 'package.json') 24 | if (!existsSync(pkgPath)) { 25 | log(red(`Could not find package.json in ${root}`)) 26 | return null 27 | } 28 | return readJSONSync(pkgPath) 29 | } 30 | 31 | export const updatePackageJson = (root: string, obj: { [key: string]: any }): any => { 32 | const pkgPath = join(process.cwd(), root, 'package.json') 33 | const pkgJson = getPackageJson(root) 34 | writeFileSync( 35 | pkgPath, 36 | JSON.stringify( 37 | { 38 | ...pkgJson, 39 | ...obj, 40 | }, 41 | null, 42 | 2, 43 | ) + '\n', 44 | ) 45 | return getPackageJson(root) 46 | } 47 | 48 | export const validatePackageJsonLicense = ( 49 | root: string, 50 | { pkgJson, license }: { pkgJson: any; license: string }, 51 | ): boolean => { 52 | // Verify that the name in package.json is correct 53 | if (!pkgJson.license) { 54 | log(red('ERROR'), `License not defined in in ${join(root, 'package.json')}`) 55 | return false 56 | } 57 | if (pkgJson.license !== license) { 58 | log( 59 | red('ERROR'), 60 | `License not valid in ${join(root, 'package.json')}, should be "${license}", not "${ 61 | pkgJson.license 62 | }"`, 63 | ) 64 | return false 65 | } 66 | return true 67 | } 68 | 69 | export const validatePackageJsonName = ( 70 | root: string, 71 | { pkgJson, name }: { pkgJson: any; name: string }, 72 | ): boolean => { 73 | if (pkgJson?.nxpm?.allowPackageName) { 74 | return true 75 | } 76 | // Verify that the name in package.json is correct 77 | if (pkgJson.name !== name) { 78 | log( 79 | red('ERROR'), 80 | `Name not valid in ${join(root, 'package.json')}, should be "${name}", not "${ 81 | pkgJson.name 82 | }" `, 83 | ) 84 | return false 85 | } 86 | return true 87 | } 88 | 89 | export const validatePackageJsonVersion = ( 90 | root: string, 91 | { pkgJson, version }: { pkgJson: any; version: string }, 92 | ): boolean => { 93 | // Verify that the version is set correctly 94 | if (!pkgJson.version || pkgJson.version !== version) { 95 | log( 96 | red('ERROR'), 97 | `Version "${pkgJson.version}" should be "${version}" in ${join(root, 'package.json')} `, 98 | ) 99 | return false 100 | } 101 | return true 102 | } 103 | 104 | export const updatePackageJsonLicense = ( 105 | root: string, 106 | { license }: { license: string }, 107 | ): boolean => { 108 | updatePackageJson(root, { license }) 109 | 110 | log(greenBright('FIXED'), `License set to ${license} in ${join(root, 'package.json')}`) 111 | return validatePackageJsonLicense(root, { pkgJson: getPackageJson(root), license }) 112 | } 113 | 114 | export const updatePackageJsonVersion = ( 115 | root: string, 116 | { version }: { version: string }, 117 | ): boolean => { 118 | updatePackageJson(root, { version }) 119 | 120 | log(greenBright('FIXED'), `Version set to "${version}" in ${join(root, 'package.json')}`) 121 | return validatePackageJsonVersion(root, { pkgJson: getPackageJson(root), version }) 122 | } 123 | 124 | export const updatePackageJsonName = (root: string, { name }: { name: string }): boolean => { 125 | updatePackageJson(root, { name }) 126 | 127 | log(greenBright('FIXED'), `Name set to "${name}" in ${join(root, 'package.json')}`) 128 | return validatePackageJsonName(root, { pkgJson: getPackageJson(root), name }) 129 | } 130 | 131 | export const validatePackageJson = ( 132 | root: string, 133 | { 134 | fix, 135 | name, 136 | version, 137 | workspacePkgJson, 138 | }: { dryRun: boolean; fix: boolean; name: string; version: string; workspacePkgJson: any }, 139 | ): boolean => { 140 | // Read the libs package.json 141 | const pkgJson = getPackageJson(root) 142 | 143 | if (!pkgJson) { 144 | return false 145 | } 146 | 147 | let hasErrors = false 148 | 149 | // Verify that the version is set correctly 150 | if (!validatePackageJsonVersion(root, { pkgJson, version })) { 151 | if (fix) { 152 | hasErrors = !updatePackageJsonVersion(root, { version }) 153 | } else { 154 | hasErrors = true 155 | } 156 | } 157 | 158 | // Verify License 159 | if (!validatePackageJsonLicense(root, { pkgJson, license: workspacePkgJson.license })) { 160 | if (fix) { 161 | hasErrors = !updatePackageJsonLicense(root, { license: workspacePkgJson.license }) 162 | } else { 163 | hasErrors = true 164 | } 165 | } 166 | 167 | // Verify name 168 | if (!validatePackageJsonName(root, { pkgJson, name })) { 169 | if (fix) { 170 | hasErrors = !updatePackageJsonName(root, { name }) 171 | } else { 172 | hasErrors = true 173 | } 174 | } 175 | 176 | if (!hasErrors) { 177 | log('VALIDATE', `Package ${yellowBright(pkgJson.name)} is valid.`) 178 | } 179 | return !hasErrors 180 | } 181 | 182 | export interface NpmPublishOptions { 183 | dryRun: boolean 184 | pkgFiles: string[] 185 | version: string 186 | local?: boolean 187 | localUrl?: string 188 | tag: 'next' | 'latest' 189 | } 190 | export const runNpmPublish = ({ 191 | dryRun, 192 | pkgFiles, 193 | version, 194 | tag, 195 | local, 196 | localUrl, 197 | }: NpmPublishOptions): boolean => { 198 | const registryUrl = local ? localUrl : 'https://registry.npmjs.org/' 199 | 200 | let hasErrors = false 201 | for (const pkgFile of pkgFiles) { 202 | const filePath = relative(process.cwd(), pkgFile) 203 | // Skip the root package.json file 204 | if (filePath !== 'package.json') { 205 | const baseDir = dirname(filePath) 206 | const pkgInfo = readJSONSync(join(process.cwd(), pkgFile)) 207 | const name = `${pkgInfo.name}@${version}` 208 | const command = `npm publish --tag ${tag} --access public --registry=${registryUrl}` 209 | if (dryRun) { 210 | log('[dry-run]', 'Skipping command', gray(command)) 211 | } else { 212 | try { 213 | exec(command, { cwd: baseDir }) 214 | } catch (error) { 215 | error(`Failed to publish ${name} to npm:`) 216 | console.log(error) 217 | hasErrors = true 218 | } 219 | } 220 | } 221 | } 222 | return !hasErrors 223 | } 224 | 225 | export interface ReleaseItOptions { 226 | ci: boolean 227 | dryRun: boolean 228 | pkgFiles: string[] 229 | preRelease: boolean 230 | version: string 231 | } 232 | export const runReleaseIt = ({ 233 | ci = false, 234 | dryRun = false, 235 | preRelease, 236 | version, 237 | }: ReleaseItOptions): Promise => { 238 | const options = { 239 | ci, 240 | 'dry-run': dryRun, 241 | changelogCommand: 'conventional-changelog -p angular | tail -n +3', 242 | /** 243 | * Needed so that we can leverage conventional-changelog to generate 244 | * the changelog 245 | */ 246 | safeBump: false, 247 | /** 248 | * All the package.json files that will have their version updated 249 | * by release-it 250 | */ 251 | increment: version, 252 | requireUpstream: false, 253 | github: { 254 | preRelease: preRelease, 255 | release: true, 256 | token: process.env.GITHUB_TOKEN, 257 | }, 258 | npm: { 259 | /** 260 | * We don't use release-it to do the npm publish, because it is not 261 | * able to understand our multi-package setup. 262 | */ 263 | release: false, 264 | publish: false, 265 | }, 266 | git: { 267 | requireCleanWorkingDir: false, 268 | }, 269 | } 270 | return releaseIt(options) 271 | .then(() => { 272 | return true 273 | }) 274 | .catch((error: any) => { 275 | error(error.message) 276 | return false 277 | }) 278 | } 279 | export const selectFromList = async ( 280 | choices: any[], 281 | { 282 | addBack = false, 283 | addExit = false, 284 | message, 285 | }: { addBack?: boolean; addExit?: boolean; message?: string }, 286 | ): Promise => { 287 | const options = [...choices] 288 | const extraOptions: string[] = [] 289 | 290 | if (addBack) { 291 | extraOptions.push(BACK_OPTION) 292 | } 293 | 294 | if (addExit) { 295 | extraOptions.push(EXIT_OPTION) 296 | } 297 | const response = await inquirer.prompt([ 298 | { 299 | name: 'select', 300 | type: 'list', 301 | message, 302 | choices: 303 | extraOptions.length === 0 304 | ? [...options] 305 | : [...options, new inquirer.Separator(), ...extraOptions, new inquirer.Separator()], 306 | }, 307 | ]) 308 | if (response.select === EXIT_OPTION) { 309 | console.clear() 310 | return false 311 | } 312 | return response.select 313 | } 314 | 315 | export async function fetchJson(url: string): Promise { 316 | return fetch(url).then((data: any) => data.json()) 317 | } 318 | 319 | export async function cacheUrls(urls: string[], cachePath: string) { 320 | mkdirpSync(dirname(cachePath)) 321 | const results = await Promise.all(urls.map(fetchJson)) 322 | const cache = urls.reduce((acc, curr, i) => ({ ...acc, [curr]: results[i] }), {}) 323 | writeJSONSync(cachePath, cache, { spaces: 2 }) 324 | } 325 | -------------------------------------------------------------------------------- /src/utils/vendor/nx-console/read-schematic-collections.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join } from 'path' 2 | import { Schematic, SchematicCollection } from './schema' 3 | 4 | import { 5 | directoryExists, 6 | fileExistsSync, 7 | listFiles, 8 | listOfUnnestedNpmPackages, 9 | normalizeSchema, 10 | readAndCacheJsonFile, 11 | readAndParseJson, 12 | } from './utils' 13 | 14 | function readWorkspaceJsonDefaults(workspaceJsonPath: string): any { 15 | const defaults = readAndParseJson(workspaceJsonPath).schematics || {} 16 | const collectionDefaults = Object.keys(defaults).reduce((collectionDefaultsMap: any, key) => { 17 | if (key.includes(':')) { 18 | const [collectionName, schematicName] = key.split(':') 19 | if (!collectionDefaultsMap[collectionName]) { 20 | collectionDefaultsMap[collectionName] = {} 21 | } 22 | collectionDefaultsMap[collectionName][schematicName] = defaults[key] 23 | } else { 24 | const collectionName = key 25 | if (!collectionDefaultsMap[collectionName]) { 26 | collectionDefaultsMap[collectionName] = {} 27 | } 28 | Object.keys(defaults[collectionName]).forEach((schematicName) => { 29 | collectionDefaultsMap[collectionName][schematicName] = 30 | defaults[collectionName][schematicName] 31 | }) 32 | } 33 | return collectionDefaultsMap 34 | }, {}) 35 | return collectionDefaults 36 | } 37 | 38 | export function canAdd( 39 | name: string, 40 | s: { hidden: boolean; private: boolean; schema: string; extends: boolean }, 41 | ): boolean { 42 | return !s.hidden && !s.private && !s.extends && name !== 'ng-add' 43 | } 44 | 45 | async function readCollectionSchematics( 46 | collectionName: string, 47 | collectionPath: string, 48 | collectionJson: any, 49 | defaults?: any, 50 | ) { 51 | const schematicCollection = { 52 | name: collectionName, 53 | schematics: [] as Schematic[], 54 | } 55 | Object.entries(collectionJson.schematics).forEach(async ([k, v]: [any, any]) => { 56 | try { 57 | if (canAdd(k, v)) { 58 | const schematicSchema = readAndCacheJsonFile(v.schema, dirname(collectionPath)) 59 | const projectDefaults = defaults && defaults[collectionName] && defaults[collectionName][k] 60 | 61 | schematicCollection.schematics.push({ 62 | name: k, 63 | collection: collectionName, 64 | options: await normalizeSchema(schematicSchema.json, projectDefaults), 65 | description: v.description || '', 66 | }) 67 | } 68 | } catch (e) { 69 | // console.error(e) 70 | console.error(`Invalid package.json for schematic ${collectionName}:${k}`) 71 | } 72 | }) 73 | return schematicCollection 74 | } 75 | 76 | async function readCollection( 77 | basedir: string, 78 | collectionName: string, 79 | defaults?: any, 80 | ): Promise { 81 | try { 82 | const packageJson = readAndCacheJsonFile(join(collectionName, 'package.json'), basedir) 83 | const collection = readAndCacheJsonFile(packageJson.json.schematics, dirname(packageJson.path)) 84 | return readCollectionSchematics(collectionName, collection.path, collection.json, defaults) 85 | } catch (e) { 86 | // this happens when package is misconfigured. We decided to ignore such a case. 87 | return null 88 | } 89 | } 90 | 91 | async function readSchematicCollectionsFromNodeModules( 92 | workspaceJsonPath: string, 93 | ): Promise { 94 | const basedir = join(workspaceJsonPath, '..') 95 | const nodeModulesDir = join(basedir, 'node_modules') 96 | const packages = listOfUnnestedNpmPackages(nodeModulesDir) 97 | const schematicCollections = packages.filter((p) => { 98 | try { 99 | return !!readAndCacheJsonFile(join(p, 'package.json'), nodeModulesDir).json.schematics 100 | } catch (e) { 101 | if ( 102 | e.message && 103 | (e.message.indexOf('no such file') > -1 || e.message.indexOf('not a directory') > -1) 104 | ) { 105 | return false 106 | } 107 | throw e 108 | } 109 | }) 110 | const defaults = readWorkspaceJsonDefaults(workspaceJsonPath) 111 | 112 | return ( 113 | await Promise.all(schematicCollections.map((c) => readCollection(nodeModulesDir, c, defaults))) 114 | ).filter((c): c is SchematicCollection => Boolean(c)) 115 | } 116 | 117 | async function readWorkspaceSchematicsCollection( 118 | basedir: string, 119 | workspaceSchematicsPath: string, 120 | ): Promise<{ 121 | name: string 122 | schematics: Schematic[] 123 | }> { 124 | const collectionDir = join(basedir, workspaceSchematicsPath) 125 | const collectionName = 'workspace-schematic' 126 | if (fileExistsSync(join(collectionDir, 'collection.json'))) { 127 | const collection = readAndCacheJsonFile('collection.json', collectionDir) 128 | 129 | return readCollectionSchematics(collectionName, collection.path, collection.json) 130 | } 131 | const schematics: Schematic[] = await Promise.all( 132 | listFiles(collectionDir) 133 | .filter((f) => basename(f) === 'schema.json') 134 | .map(async (f) => { 135 | const schemaJson = readAndCacheJsonFile(f, '') 136 | return { 137 | name: schemaJson.json.id, 138 | collection: collectionName, 139 | options: await normalizeSchema(schemaJson.json), 140 | description: '', 141 | } 142 | }), 143 | ) 144 | return { name: collectionName, schematics } 145 | } 146 | 147 | export async function readAllSchematicCollections( 148 | workspaceJsonPath: string, 149 | workspaceSchematicsPath: string = join('tools', 'schematics'), 150 | ): Promise { 151 | const basedir = join(workspaceJsonPath, '..') 152 | let collections = await readSchematicCollectionsFromNodeModules(workspaceJsonPath) 153 | if (directoryExists(join(basedir, workspaceSchematicsPath))) { 154 | collections = [ 155 | await readWorkspaceSchematicsCollection(basedir, workspaceSchematicsPath), 156 | ...collections, 157 | ] 158 | } 159 | return collections.filter( 160 | (collection): collection is SchematicCollection => 161 | !!collection && collection!.schematics!.length > 0, 162 | ) 163 | } 164 | -------------------------------------------------------------------------------- /src/utils/vendor/nx-console/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Option { 2 | [key: string]: any 3 | } 4 | 5 | export interface SchematicCollection { 6 | name: string 7 | schematics: Schematic[] 8 | } 9 | 10 | export interface Schematic { 11 | collection: string 12 | name: string 13 | description: string 14 | options: Option[] 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/vendor/nx-console/utils.ts: -------------------------------------------------------------------------------- 1 | import { schema } from '@angular-devkit/core' 2 | import { standardFormats } from '@angular-devkit/schematics/src/formats' 3 | import { Option } from '@angular/cli/models/interface' 4 | import { parseJsonSchemaToOptions } from '@angular/cli/utilities/json-schema' 5 | import { existsSync, readdirSync, readFileSync, statSync } from 'fs' 6 | import * as JSON5 from 'json5' 7 | import * as path from 'path' 8 | 9 | export interface SchematicDefaults { 10 | [name: string]: string 11 | } 12 | 13 | export const files: { [path: string]: string[] } = {} 14 | export const fileContents: { [path: string]: any } = {} 15 | 16 | const IMPORTANT_FIELD_NAMES = ['name', 'project', 'module', 'watch', 'style', 'directory', 'port'] 17 | const IMPORTANT_FIELDS_SET = new Set(IMPORTANT_FIELD_NAMES) 18 | 19 | export function listOfUnnestedNpmPackages(nodeModulesDir: string): string[] { 20 | const res: string[] = [] 21 | if (!existsSync(nodeModulesDir)) { 22 | return res 23 | } 24 | 25 | readdirSync(nodeModulesDir).forEach((npmPackageOrScope) => { 26 | if (npmPackageOrScope.startsWith('@')) { 27 | readdirSync(path.join(nodeModulesDir, npmPackageOrScope)).forEach((p) => { 28 | res.push(`${npmPackageOrScope}/${p}`) 29 | }) 30 | } else { 31 | res.push(npmPackageOrScope) 32 | } 33 | }) 34 | return res 35 | } 36 | 37 | export function listFiles(dirName: string): string[] { 38 | // TODO use .gitignore to skip files 39 | if (dirName.indexOf('node_modules') > -1) return [] 40 | if (dirName.indexOf('dist') > -1) return [] 41 | 42 | const res = [dirName] 43 | // the try-catch here is intentional. It's only used in auto-completion. 44 | // If it doesn't work, we don't want the process to exit 45 | try { 46 | readdirSync(dirName).forEach((c) => { 47 | const child = path.join(dirName, c) 48 | try { 49 | if (!statSync(child).isDirectory()) { 50 | res.push(child) 51 | } else if (statSync(child).isDirectory()) { 52 | res.push(...listFiles(child)) 53 | } 54 | } catch (e) {} 55 | }) 56 | } catch (e) {} 57 | return res 58 | } 59 | 60 | export function directoryExists(filePath: string): boolean { 61 | try { 62 | return statSync(filePath).isDirectory() 63 | } catch (err) { 64 | return false 65 | } 66 | } 67 | 68 | export function fileExistsSync(filePath: string): boolean { 69 | try { 70 | return statSync(filePath).isFile() 71 | } catch (err) { 72 | return false 73 | } 74 | } 75 | 76 | export function readAndParseJson(fullFilePath: string): any { 77 | return JSON5.parse(readFileSync(fullFilePath).toString()) 78 | } 79 | 80 | export function readAndCacheJsonFile( 81 | filePath: string, 82 | basedir: string, 83 | ): { path: string; json: any } { 84 | const fullFilePath = path.join(basedir, filePath) 85 | 86 | if (fileContents[fullFilePath] || existsSync(fullFilePath)) { 87 | fileContents[fullFilePath] = fileContents[fullFilePath] || readAndParseJson(fullFilePath) 88 | 89 | return { 90 | path: fullFilePath, 91 | json: fileContents[fullFilePath], 92 | } 93 | } 94 | return { 95 | path: fullFilePath, 96 | json: {}, 97 | } 98 | } 99 | 100 | const registry = new schema.CoreSchemaRegistry(standardFormats) 101 | export async function normalizeSchema( 102 | s: { 103 | properties: { [k: string]: any } 104 | required: string[] 105 | }, 106 | projectDefaults?: SchematicDefaults, 107 | ): Promise { 108 | const options = await parseJsonSchemaToOptions(registry, s) 109 | const requiredFields = new Set(s.required || []) 110 | 111 | options.forEach((option) => { 112 | const workspaceDefault = projectDefaults && projectDefaults[option.name] 113 | 114 | if (workspaceDefault !== undefined) { 115 | option.default = workspaceDefault 116 | } 117 | 118 | if (requiredFields.has(option.name)) { 119 | option.required = true 120 | } 121 | }) 122 | 123 | return options.sort((a, b) => { 124 | if (typeof a.positional === 'number' && typeof b.positional === 'number') { 125 | return a.positional - b.positional 126 | } 127 | 128 | if (typeof a.positional === 'number') { 129 | return -1 130 | } else if (typeof b.positional === 'number') { 131 | return 1 132 | } else if (a.required) { 133 | if (b.required) { 134 | return a.name.localeCompare(b.name) 135 | } 136 | return -1 137 | } else if (b.required) { 138 | return 1 139 | } else if (IMPORTANT_FIELDS_SET.has(a.name)) { 140 | if (IMPORTANT_FIELDS_SET.has(b.name)) { 141 | return IMPORTANT_FIELD_NAMES.indexOf(a.name) - IMPORTANT_FIELD_NAMES.indexOf(b.name) 142 | } 143 | return -1 144 | } else if (IMPORTANT_FIELDS_SET.has(b.name)) { 145 | return 1 146 | } else { 147 | return a.name.localeCompare(b.name) 148 | } 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /test/commands/config/delete.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('config:delete', () => { 4 | test 5 | .stdout() 6 | .command(['config:delete']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['config:delete', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/config/edit.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('config:edit', () => { 4 | test 5 | .stdout() 6 | .command(['config:edit']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['config:edit', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/config/get.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('config:get', () => { 4 | test 5 | .stdout() 6 | .command(['config:get']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['config:get', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/config/set.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('config:set', () => { 4 | test 5 | .stdout() 6 | .command(['config:set']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['config:set', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/plugins.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('plugins', () => { 4 | test 5 | .stdout() 6 | .command(['plugins']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['plugins', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/projects.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('projects', () => { 4 | test 5 | .stdout() 6 | .command(['projects']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['projects', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/registry/disable.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('registry:disable', () => { 4 | test 5 | .stdout() 6 | .command(['registry:disable']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['registry:disable', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/registry/enable.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('registry:enable', () => { 4 | test 5 | .stdout() 6 | .command(['registry:enable']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['registry:enable', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/registry/start.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('registry:start', () => { 4 | test 5 | .stdout() 6 | .command(['registry:start']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['registry:start', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/registry/status.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('registry:status', () => { 4 | test 5 | .stdout() 6 | .command(['registry:status']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['registry:status', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/release.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('release', () => { 4 | test 5 | .stdout() 6 | .command(['release']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['release', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/sandbox.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('sandbox', () => { 4 | test 5 | .stdout() 6 | .command(['sandbox']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['sandbox', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/sandbox/pull.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('sandbox:pull', () => { 4 | test 5 | .stdout() 6 | .command(['sandbox:pull']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['sandbox:pull', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --watch-extensions ts 3 | --recursive 4 | --reporter spec 5 | --timeout 5000 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "strict": false, 11 | "lib": ["es2017", "es2019"], 12 | "target": "es2017" 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | --------------------------------------------------------------------------------